Skip to content

Commit 359546c

Browse files
committed
More robust type inference for d3.interpolate.
Fixes d3#1180 d3#1179 d3#1176. Still a work in progress; needs a few more tests.
1 parent 1810583 commit 359546c

10 files changed

Lines changed: 150 additions & 57 deletions

File tree

d3.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4683,7 +4683,7 @@ d3 = function() {
46834683
};
46844684
d3.interpolateNumber = d3_interpolateNumber;
46854685
function d3_interpolateNumber(a, b) {
4686-
b -= a;
4686+
b -= a = +a;
46874687
return function(t) {
46884688
return a + b * t;
46894689
};
@@ -4764,6 +4764,7 @@ d3 = function() {
47644764
d3.interpolateString = d3_interpolateString;
47654765
function d3_interpolateString(a, b) {
47664766
var m, i, j, s0 = 0, s1 = 0, s = [], q = [], n, o;
4767+
a = a + "", b = b + "";
47674768
d3_interpolate_number.lastIndex = 0;
47684769
for (i = 0; m = d3_interpolate_number.exec(b); ++i) {
47694770
if (m.index) s.push(b.substring(s0, s1 = m.index));
@@ -4834,14 +4835,9 @@ d3 = function() {
48344835
function d3_interpolateByName(name) {
48354836
return name == "transform" ? d3_interpolateTransform : d3_interpolate;
48364837
}
4837-
d3.interpolators = [ d3_interpolateObject, function(a, b) {
4838-
return Array.isArray(b) && d3_interpolateArray(a, b);
4839-
}, function(a, b) {
4840-
return (typeof a === "string" || typeof b === "string") && d3_interpolateString(a + "", b + "");
4841-
}, function(a, b) {
4842-
return (typeof b === "string" ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) : b instanceof d3_Color) && d3_interpolateRgb(a, b);
4843-
}, function(a, b) {
4844-
return !isNaN(a = +a) && !isNaN(b = +b) && d3_interpolateNumber(a, b);
4838+
d3.interpolators = [ function(a, b) {
4839+
var t = typeof b;
4840+
return (t === "string" || t !== typeof a ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) ? d3_interpolateRgb : d3_interpolateString : b instanceof d3_Color ? d3_interpolateRgb : t === "object" ? Array.isArray(b) ? d3_interpolateArray : d3_interpolateObject : d3_interpolateNumber)(a, b);
48454841
} ];
48464842
d3.interpolateArray = d3_interpolateArray;
48474843
function d3_interpolateArray(a, b) {

d3.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/interpolate/interpolate.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ function d3_interpolateByName(name) {
2222
}
2323

2424
d3.interpolators = [
25-
d3_interpolateObject,
26-
function(a, b) { return Array.isArray(b) && d3_interpolateArray(a, b); },
27-
function(a, b) { return (typeof a === "string" || typeof b === "string") && d3_interpolateString(a + "", b + ""); },
28-
function(a, b) { return (typeof b === "string" ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) : b instanceof d3_Color) && d3_interpolateRgb(a, b); },
29-
function(a, b) { return !isNaN(a = +a) && !isNaN(b = +b) && d3_interpolateNumber(a, b); }
25+
function(a, b) {
26+
var t = typeof b;
27+
return (t === "string" || t !== typeof a ? (d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) ? d3_interpolateRgb : d3_interpolateString)
28+
: b instanceof d3_Color ? d3_interpolateRgb
29+
: t === "object" ? (Array.isArray(b) ? d3_interpolateArray : d3_interpolateObject)
30+
: d3_interpolateNumber)(a, b);
31+
}
3032
];

src/interpolate/number.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
d3.interpolateNumber = d3_interpolateNumber;
22

33
function d3_interpolateNumber(a, b) {
4-
b -= a;
4+
b -= a = +a;
55
return function(t) { return a + b * t; };
66
}

src/interpolate/string.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ function d3_interpolateString(a, b) {
1313
n, // q.length
1414
o;
1515

16+
// Coerce inputs to strings.
17+
a = a + "", b = b + "";
18+
1619
// Reset our regular expression!
1720
d3_interpolate_number.lastIndex = 0;
1821

test/assert.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ var assert = require("assert");
22

33
assert = module.exports = Object.create(assert);
44

5+
assert.isArray = function(actual, message) {
6+
if (!Array.isArray(actual)) {
7+
assert.fail(actual, null, message || "expected {actual} to be an Array", null, assert.isArray);
8+
}
9+
};
10+
511
assert.inDelta = function(actual, expected, delta, message) {
612
if (!inDelta(actual, expected, delta)) {
713
assert.fail(actual, expected, message || "expected {actual} to be in within *" + delta + "* of {expected}", null, assert.inDelta);

test/interpolate/interpolate-test.js

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,102 @@ var suite = vows.describe("d3.interpolate");
77
suite.addBatch({
88
"interpolate": {
99
topic: load("interpolate/interpolate").document(),
10-
"interpolates numbers": function(d3) {
11-
assert.equal(d3.interpolate(2, 12)(.4), 6);
12-
assert.equal(d3.interpolate("2px", 12)(.4), 6);
10+
11+
"when a and b are numbers": {
12+
"interpolates numbers": function(d3) {
13+
assert.strictEqual(d3.interpolate(2, 12)(.4), 6);
14+
}
15+
},
16+
17+
"when a and b are color strings": {
18+
"interpolates RGB values and returns a hexadecimal string": function(d3) {
19+
assert.strictEqual(d3.interpolate("#ff0000", "#008000")(.4), "#993300");
20+
},
21+
"interpolates named colors in RGB": function(d3) {
22+
assert.strictEqual(d3.interpolate("red", "green")(.4), "#993300");
23+
},
24+
"interpolates decimal RGB colors in RGB": function(d3) {
25+
assert.strictEqual(d3.interpolate("rgb(255,0,0)", "rgb(0,128,0)")(.4), "#993300");
26+
},
27+
"interpolates decimal HSL colors in RGB": function(d3) {
28+
assert.strictEqual(d3.interpolate("hsl(0,100%,50%)", "hsl(120,100%,25%)")(.4), "#993300");
29+
}
1330
},
14-
"interpolates colors": function(d3) { // beware instanceof d3_Color
15-
assert.equal(d3.interpolate("#abcdef", "#fedcba")(.4), "#ccd3da");
16-
assert.equal(d3.interpolate("#abcdef", d3.rgb("#fedcba"))(.4), "#ccd3da");
17-
assert.equal(d3.interpolate("#abcdef", d3.hsl("#fedcba"))(.4), "#ccd3da");
18-
assert.equal(d3.interpolate("#abcdef", d3.lab("#fedcba"))(.4), "#ccd3da");
31+
32+
"when a and b are color objects": {
33+
"interpolates RGB values and returns a hexadecimal string": function(d3) {
34+
assert.strictEqual(d3.interpolate(d3.rgb(255, 0, 0), d3.rgb(0, 128, 0))(.4), "#993300");
35+
},
36+
"interpolates d3.hsl in RGB": function(d3) {
37+
assert.strictEqual(d3.interpolate(d3.hsl("red"), d3.hsl("green"))(.4), "#993300");
38+
},
39+
"interpolates d3.lab in RGB": function(d3) {
40+
assert.strictEqual(d3.interpolate(d3.lab("red"), d3.lab("green"))(.4), "#993300");
41+
},
42+
"interpolates d3.hcl in RGB": function(d3) {
43+
assert.strictEqual(d3.interpolate(d3.hcl("red"), d3.hcl("green"))(.4), "#993300");
44+
}
45+
},
46+
47+
"when a and b are strings": {
48+
"interpolates matching numbers in both strings": function(d3) {
49+
assert.strictEqual(d3.interpolate(" 10/20 30", "50/10 100 ")(.4), "26/16 58 ");
50+
},
51+
"if a and b are coercible to numbers, interpolates numbers rather than strings": function(d3) {
52+
assert.strictEqual(d3.interpolate("1.", "2.")(.5), 1.5);
53+
assert.strictEqual(d3.interpolate("1e+3", "1e+4")(.5), 5500);
54+
},
55+
"preserves non-numbers in string b": function(d3) {
56+
assert.strictEqual(d3.interpolate(" 10/20 30", "50/10 foo ")(.4), "26/16 foo ");
57+
},
58+
"preserves non-matching numbers in string b": function(d3) {
59+
assert.strictEqual(d3.interpolate(" 10/20 bar", "50/10 100 ")(.4), "26/16 100 ");
60+
},
61+
"preserves equal-value numbers in both strings": function(d3) {
62+
assert.strictEqual(d3.interpolate(" 10/20 100 20", "50/10 100, 20 ")(.4), "26/16 100, 20 ");
63+
}
1964
},
20-
"interpolates strings": function(d3) {
21-
assert.equal(d3.interpolate("width:10px;", "width:50px;")(.2), "width:18px;");
22-
assert.equal(d3.interpolate(2, "12px")(.4), "6px");
65+
66+
"when a and b are arrays": {
67+
"interpolates each element in b": function(d3) {
68+
assert.strictEqual(JSON.stringify(d3.interpolate([2, 4], [12, 24])(.4)), "[6,12]");
69+
},
70+
"interpolates arrays, even when both a and b are coercible to numbers": function(d3) {
71+
assert.strictEqual(JSON.stringify(d3.interpolate([2], [12])(.4)), "[6]");
72+
assert.strictEqual(JSON.stringify(d3.interpolate([[2]], [[12]])(.4)), "[[6]]");
73+
},
74+
"reuses the returned array during interpolation": function(d3) {
75+
var i = d3.interpolate([2], [12]);
76+
assert.strictEqual(i(.2), i(.4));
77+
}
2378
},
24-
"interpolates arrays": function(d3) {
25-
assert.deepEqual(d3.interpolate([2, 4], [12, 24])(.4), [6, 12]);
79+
80+
"when a and b are objects": {
81+
"interpolates each property in b": function(d3) {
82+
assert.deepEqual(d3.interpolate({foo: 2, bar: 4}, {foo: 12, bar: 24})(.4), {foo: 6, bar: 12});
83+
},
84+
"interpolates arrays, even when both a and b are coercible to numbers": function(d3) {
85+
var two = new Number(2), twelve = new Number(12);
86+
two.foo = "2px";
87+
twelve.foo = "12px";
88+
assert.deepEqual(d3.interpolate(two, twelve)(.4), {foo: "6px"});
89+
},
90+
"reuses the returned object during interpolation": function(d3) {
91+
var i = d3.interpolate({foo: 2, bar: 4}, {foo: 12, bar: 24});
92+
assert.strictEqual(i(.2), i(.4));
93+
}
2694
},
27-
"interpolates objects": function(d3) {
28-
assert.deepEqual(d3.interpolate({foo: 2}, {foo: 12})(.4), {foo: 6});
95+
96+
"when a and b are different types": {
97+
"coerces both types to strings": function(d3) {
98+
assert.strictEqual(d3.interpolate("2", 12)(.4), 6);
99+
assert.strictEqual(d3.interpolate("2px", 12)(.4), 6);
100+
assert.strictEqual(d3.interpolate([2], 12)(.4), 6);
101+
assert.strictEqual(d3.interpolate({valueOf: function() { return 2; }}, 12)(.4), 6);
102+
assert.strictEqual(d3.interpolate({toString: function() { return 2; }}, 12)(.4), 6);
103+
}
29104
},
105+
30106
"may or may not interpolate between enumerable and non-enumerable properties": function(d3) {
31107
var a = Object.create({}, {foo: {value: 1, enumerable: true}}),
32108
b = Object.create({}, {foo: {value: 2, enumerable: false}});
@@ -50,17 +126,20 @@ suite.addBatch({
50126
assert.equal(d3.interpolate("hasOwnProperty", "hasOwnProperty")(0), "hasOwnProperty");
51127
}
52128
},
129+
53130
"interpolators": {
54131
topic: load("interpolate/interpolate").document(),
55132
"can register a custom interpolator": function(d3) {
56-
d3.interpolators.push(function(a, b) {
57-
return a == "one" && b == "two" && d3.interpolateNumber(1, 2);
58-
});
59-
assert.equal(d3.interpolate("one", "two")(-.5), .5);
60-
assert.equal(d3.interpolate("one", "two")(0), 1);
61-
assert.equal(d3.interpolate("one", "two")(.5), 1.5);
62-
assert.equal(d3.interpolate("one", "two")(1), 2);
63-
assert.equal(d3.interpolate("one", "two")(1.5), 2.5);
133+
d3.interpolators.push(function(a, b) { return a == "one" && b == "two" && d3.interpolateNumber(1, 2); });
134+
try {
135+
assert.equal(d3.interpolate("one", "two")(-.5), .5);
136+
assert.equal(d3.interpolate("one", "two")(0), 1);
137+
assert.equal(d3.interpolate("one", "two")(.5), 1.5);
138+
assert.equal(d3.interpolate("one", "two")(1), 2);
139+
assert.equal(d3.interpolate("one", "two")(1.5), 2.5);
140+
} finally {
141+
d3.interpolators.pop();
142+
}
64143
}
65144
}
66145
});

test/interpolate/number-test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ suite.addBatch({
88
"interpolateNumber": {
99
topic: load("interpolate/number").expression("d3.interpolateNumber"),
1010
"interpolates numbers": function(interpolate) {
11-
assert.equal(interpolate(2, 12)(.4), 6);
12-
assert.equal(interpolate(2, 12)(.6), 8);
11+
assert.strictEqual(interpolate(2, 12)(.4), 6);
12+
assert.strictEqual(interpolate(2, 12)(.6), 8);
13+
},
14+
"coerces strings to numbers": function(interpolate) {
15+
assert.strictEqual(interpolate("2", "12")(.4), 6);
1316
}
1417
}
1518
});

test/interpolate/string-test.js

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,34 @@ suite.addBatch({
88
"interpolateString": {
99
topic: load("interpolate/string").expression("d3.interpolateString"),
1010
"interpolates matching numbers in both strings": function(interpolate) {
11-
assert.equal(interpolate(" 10/20 30", "50/10 100 ")(.2), "18/18 44 ");
12-
assert.equal(interpolate(" 10/20 30", "50/10 100 ")(.4), "26/16 58 ");
11+
assert.strictEqual(interpolate(" 10/20 30", "50/10 100 ")(.2), "18/18 44 ");
12+
assert.strictEqual(interpolate(" 10/20 30", "50/10 100 ")(.4), "26/16 58 ");
13+
},
14+
"coerces objects to strings": function(interpolate) {
15+
assert.strictEqual(interpolate({toString: function() { return "2px"; }}, {toString: function() { return "12px"; }})(.4), "6px");
1316
},
1417
"preserves non-numbers in string b": function(interpolate) {
15-
assert.equal(interpolate(" 10/20 30", "50/10 foo ")(.2), "18/18 foo ");
16-
assert.equal(interpolate(" 10/20 30", "50/10 foo ")(.4), "26/16 foo ");
18+
assert.strictEqual(interpolate(" 10/20 30", "50/10 foo ")(.2), "18/18 foo ");
19+
assert.strictEqual(interpolate(" 10/20 30", "50/10 foo ")(.4), "26/16 foo ");
1720
},
1821
"preserves non-matching numbers in string b": function(interpolate) {
19-
assert.equal(interpolate(" 10/20 foo", "50/10 100 ")(.2), "18/18 100 ");
20-
assert.equal(interpolate(" 10/20 bar", "50/10 100 ")(.4), "26/16 100 ");
22+
assert.strictEqual(interpolate(" 10/20 foo", "50/10 100 ")(.2), "18/18 100 ");
23+
assert.strictEqual(interpolate(" 10/20 bar", "50/10 100 ")(.4), "26/16 100 ");
2124
},
2225
"preserves equal-value numbers in both strings": function(interpolate) {
23-
assert.equal(interpolate(" 10/20 100 20", "50/10 100, 20 ")(.2), "18/18 100, 20 ");
24-
assert.equal(interpolate(" 10/20 100 20", "50/10 100, 20 ")(.4), "26/16 100, 20 ");
26+
assert.strictEqual(interpolate(" 10/20 100 20", "50/10 100, 20 ")(.2), "18/18 100, 20 ");
27+
assert.strictEqual(interpolate(" 10/20 100 20", "50/10 100, 20 ")(.4), "26/16 100, 20 ");
2528
},
2629
"interpolates decimal notation correctly": function(interpolate) {
27-
assert.equal(interpolate("1.", "2.")(.5), "1.5");
30+
assert.strictEqual(interpolate("1.", "2.")(.5), 1.5);
2831
},
2932
"interpolates exponent notation correctly": function(interpolate) {
30-
assert.equal(interpolate("1e+3", "1e+4")(.5), "5500");
31-
assert.equal(interpolate("1e-3", "1e-4")(.5), "0.00055");
32-
assert.equal(interpolate("1.e-3", "1.e-4")(.5), "0.00055");
33-
assert.equal(interpolate("-1.e-3", "-1.e-4")(.5), "-0.00055");
34-
assert.equal(interpolate("+1.e-3", "+1.e-4")(.5), "0.00055");
35-
assert.equal(interpolate(".1e-2", ".1e-3")(.5), "0.00055");
33+
assert.strictEqual(interpolate("1e+3", "1e+4")(.5), 5500);
34+
assert.strictEqual(interpolate("1e-3", "1e-4")(.5), 0.00055);
35+
assert.strictEqual(interpolate("1.e-3", "1.e-4")(.5), 0.00055);
36+
assert.strictEqual(interpolate("-1.e-3", "-1.e-4")(.5), -0.00055);
37+
assert.strictEqual(interpolate("+1.e-3", "+1.e-4")(.5), 0.00055);
38+
assert.strictEqual(interpolate(".1e-2", ".1e-3")(.5), 0.00055);
3639
}
3740
}
3841
});

test/load.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ module.exports = function() {
3434
};
3535

3636
sandbox = {
37+
console: console,
3738
XMLHttpRequest: XMLHttpRequest,
3839
document: document,
3940
window: document.createWindow(),

0 commit comments

Comments
 (0)