Skip to content

Commit ff5364d

Browse files
committed
Allow hue, chroma or saturation to be undefined.
In HSL space, grayscale colors can now have undefined hue rather than assuming a hue of 0°; likewise black and white can have undefined saturation rather than assuming 0%. In HCL space, black can now have undefined hue and chroma. (For non-black grayscale colors, including white, hue and chroma are implied by the D65 standard referent.) When interpolating between colors with undefined hue, saturation or chroma, the defined value is used when available. For example, when interpolating from black to blue in HCL space, the intermediate colors are now dark blues (#241178) rather than dark purples (#600054). Fixes d3#833.
1 parent 9f25c5f commit ff5364d

12 files changed

Lines changed: 171 additions & 32 deletions

File tree

d3.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,9 +1264,8 @@ d3 = function() {
12641264
};
12651265
function d3_hsl_rgb(h, s, l) {
12661266
var m1, m2;
1267-
h = h % 360;
1268-
if (h < 0) h += 360;
1269-
s = s < 0 ? 0 : s > 1 ? 1 : s;
1267+
h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h;
1268+
s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s;
12701269
l = l < 0 ? 0 : l > 1 ? 1 : l;
12711270
m2 = l <= .5 ? l * (1 + s) : l + s - l * s;
12721271
m1 = 2 * l - m2;
@@ -1323,6 +1322,8 @@ d3 = function() {
13231322
return d3_hcl_lab(this.h, this.c, this.l).rgb();
13241323
};
13251324
function d3_hcl_lab(h, c, l) {
1325+
if (isNaN(h)) h = 0;
1326+
if (isNaN(c)) c = 0;
13261327
return d3_lab(l, Math.cos(h *= d3_radians) * c, Math.sin(h) * c);
13271328
}
13281329
d3.lab = function(l, a, b) {
@@ -1356,7 +1357,7 @@ d3 = function() {
13561357
return d3_rgb(d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - .4985314 * z), d3_xyz_rgb(-.969266 * x + 1.8760108 * y + .041556 * z), d3_xyz_rgb(.0556434 * x - .2040259 * y + 1.0572252 * z));
13571358
}
13581359
function d3_lab_hcl(l, a, b) {
1359-
return d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l);
1360+
return l > 0 ? d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l) : d3_hcl(NaN, NaN, l);
13601361
}
13611362
function d3_lab_xyz(x) {
13621363
return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037;
@@ -1439,13 +1440,11 @@ d3 = function() {
14391440
return rgb(r, g, b);
14401441
}
14411442
function d3_rgb_hsl(r, g, b) {
1442-
var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h, s, l = (max + min) / 2;
1443+
var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h = NaN, s = h, l = (max + min) / 2;
14431444
if (d) {
14441445
s = l < .5 ? d / (max + min) : d / (2 - max - min);
14451446
if (r == max) h = (g - b) / d + (g < b ? 6 : 0); else if (g == max) h = (b - r) / d + 2; else h = (r - g) / d + 4;
14461447
h *= 60;
1447-
} else {
1448-
s = h = 0;
14491448
}
14501449
return d3_hsl(h, s, l);
14511450
}
@@ -4956,7 +4955,8 @@ d3 = function() {
49564955
a = d3.hcl(a);
49574956
b = d3.hcl(b);
49584957
var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al;
4959-
if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;
4958+
if (isNaN(bc)) bc = 0, ac = isNaN(ac) ? b.c : ac;
4959+
if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;
49604960
return function(t) {
49614961
return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + "";
49624962
};
@@ -4965,10 +4965,11 @@ d3 = function() {
49654965
function d3_interpolateHsl(a, b) {
49664966
a = d3.hsl(a);
49674967
b = d3.hsl(b);
4968-
var h0 = a.h, s0 = a.s, l0 = a.l, h1 = b.h - h0, s1 = b.s - s0, l1 = b.l - l0;
4969-
if (h1 > 180) h1 -= 360; else if (h1 < -180) h1 += 360;
4968+
var ah = a.h, as = a.s, al = a.l, bh = b.h - ah, bs = b.s - as, bl = b.l - al;
4969+
if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as;
4970+
if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;
49704971
return function(t) {
4971-
return d3_hsl_rgb(h0 + h1 * t, s0 + s1 * t, l0 + l1 * t) + "";
4972+
return d3_hsl_rgb(ah + bh * t, as + bs * t, al + bl * t) + "";
49724973
};
49734974
}
49744975
d3.interpolateLab = d3_interpolateLab;

d3.min.js

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

src/color/hcl.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,7 @@ d3_hclPrototype.rgb = function() {
3636
};
3737

3838
function d3_hcl_lab(h, c, l) {
39+
if (isNaN(h)) h = 0;
40+
if (isNaN(c)) c = 0;
3941
return d3_lab(l, Math.cos(h *= d3_radians) * c, Math.sin(h) * c);
4042
}

src/color/hsl.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ function d3_hsl_rgb(h, s, l) {
3939
m2;
4040

4141
/* Some simple corrections for h, s and l. */
42-
h = h % 360; if (h < 0) h += 360;
43-
s = s < 0 ? 0 : s > 1 ? 1 : s;
42+
h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h;
43+
s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s;
4444
l = l < 0 ? 0 : l > 1 ? 1 : l;
4545

4646
/* From FvD 13.37, CSS Color Module Level 3 */

src/color/lab.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ function d3_lab_rgb(l, a, b) {
5858
}
5959

6060
function d3_lab_hcl(l, a, b) {
61-
return d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l);
61+
return l > 0
62+
? d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l)
63+
: d3_hcl(NaN, NaN, l);
6264
}
6365

6466
function d3_lab_xyz(x) {

src/color/rgb.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,15 @@ function d3_rgb_hsl(r, g, b) {
117117
var min = Math.min(r /= 255, g /= 255, b /= 255),
118118
max = Math.max(r, g, b),
119119
d = max - min,
120-
h,
121-
s,
120+
h = NaN,
121+
s = h,
122122
l = (max + min) / 2;
123123
if (d) {
124124
s = l < .5 ? d / (max + min) : d / (2 - max - min);
125125
if (r == max) h = (g - b) / d + (g < b ? 6 : 0);
126126
else if (g == max) h = (b - r) / d + 2;
127127
else h = (r - g) / d + 4;
128128
h *= 60;
129-
} else {
130-
s = h = 0;
131129
}
132130
return d3_hsl(h, s, l);
133131
}

src/interpolate/hcl.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ function d3_interpolateHcl(a, b) {
1111
bh = b.h - ah,
1212
bc = b.c - ac,
1313
bl = b.l - al;
14-
if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; // shortest path
14+
if (isNaN(bc)) bc = 0, ac = isNaN(ac) ? b.c : ac;
15+
if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah;
16+
else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; // shortest path
1517
return function(t) {
1618
return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + "";
1719
};

src/interpolate/hsl.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ d3.interpolateHsl = d3_interpolateHsl;
77
function d3_interpolateHsl(a, b) {
88
a = d3.hsl(a);
99
b = d3.hsl(b);
10-
var h0 = a.h,
11-
s0 = a.s,
12-
l0 = a.l,
13-
h1 = b.h - h0,
14-
s1 = b.s - s0,
15-
l1 = b.l - l0;
16-
if (h1 > 180) h1 -= 360; else if (h1 < -180) h1 += 360; // shortest path
10+
var ah = a.h,
11+
as = a.s,
12+
al = a.l,
13+
bh = b.h - ah,
14+
bs = b.s - as,
15+
bl = b.l - al;
16+
if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as;
17+
if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah;
18+
else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; // shortest path
1719
return function(t) {
18-
return d3_hsl_rgb(h0 + h1 * t, s0 + s1 * t, l0 + l1 * t) + "";
20+
return d3_hsl_rgb(ah + bh * t, as + bs * t, al + bl * t) + "";
1921
};
2022
}

test/color/hcl-test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,37 @@ suite.addBatch({
7474
},
7575
"roundtrip to Lab is idempotent": function(hcl) {
7676
assert.hslEqual(_.lab(hcl("steelblue")), _.lab("steelblue"));
77+
},
78+
"h is defined for non-black grayscale colors (because of the color profile)": function(hcl) {
79+
assert.inDelta(hcl("#ccc").h, 158.1986, 1e-3);
80+
assert.inDelta(hcl("gray").h, 158.1986, 1e-3);
81+
assert.inDelta(hcl(_.rgb("gray")).h, 158.1986, 1e-3);
82+
assert.inDelta(hcl("#fff").h, 158.1986, 1e-3);
83+
assert.inDelta(hcl("white").h, 158.1986, 1e-3);
84+
assert.inDelta(hcl(_.rgb("white")).h, 158.1986, 1e-3);
85+
},
86+
"h is preserved when explicitly specified, even for black": function(hcl) {
87+
assert.strictEqual(hcl(0, 0, 0).h, 0);
88+
assert.strictEqual(hcl(42, 0, 0).h, 42);
89+
assert.strictEqual(hcl(118, 0, 0).h, 118);
90+
},
91+
"h is undefined when not explicitly specified for black": function(hcl) {
92+
assert.isNaN(hcl("#000").h);
93+
assert.isNaN(hcl("black").h);
94+
assert.isNaN(hcl(_.rgb("black")).h);
95+
},
96+
"c is preserved when explicitly specified, even for black": function(hcl) {
97+
assert.strictEqual(hcl(0, 0, 0).c, 0);
98+
assert.strictEqual(hcl(0, .42, 0).c, .42);
99+
assert.strictEqual(hcl(0, 1, 0).c, 1);
100+
},
101+
"c is undefined when not explicitly specified for black": function(hcl) {
102+
assert.isNaN(hcl("#000").c);
103+
assert.isNaN(hcl("black").c);
104+
assert.isNaN(hcl(_.rgb("black")).c);
105+
},
106+
"can convert black (with undefined hue and chroma) to RGB": function(hcl) {
107+
assert.strictEqual(hcl(NaN, NaN, 0) + "", "#000000");
77108
}
78109
}
79110
});

test/color/hsl-test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,44 @@ suite.addBatch({
7272
"string coercion returns RGB format": function(hsl) {
7373
assert.strictEqual(hsl("hsl(60, 100%, 20%)") + "", "#666600");
7474
assert.strictEqual(hsl(hsl(60, 1, .2)) + "", "#666600");
75+
},
76+
"h is preserved when explicitly specified, even for grayscale colors": function(hsl) {
77+
assert.strictEqual(hsl(0, 0, 0).h, 0);
78+
assert.strictEqual(hsl(42, 0, .5).h, 42);
79+
assert.strictEqual(hsl(118, 0, 1).h, 118);
80+
},
81+
"h is undefined when not explicitly specified for grayscale colors": function(hsl) {
82+
assert.isNaN(hsl("#000").h);
83+
assert.isNaN(hsl("black").h);
84+
assert.isNaN(hsl(_.rgb("black")).h);
85+
assert.isNaN(hsl("#ccc").h);
86+
assert.isNaN(hsl("gray").h);
87+
assert.isNaN(hsl(_.rgb("gray")).h);
88+
assert.isNaN(hsl("#fff").h);
89+
assert.isNaN(hsl("white").h);
90+
assert.isNaN(hsl(_.rgb("white")).h);
91+
},
92+
"s is preserved when explicitly specified, even for white or black": function(hsl) {
93+
assert.strictEqual(hsl(0, 0, 0).s, 0);
94+
assert.strictEqual(hsl(0, .18, 0).s, .18);
95+
assert.strictEqual(hsl(0, .42, 1).s, .42);
96+
assert.strictEqual(hsl(0, 1, 1).s, 1);
97+
},
98+
"s is undefined when not explicitly specified for white or black": function(hsl) {
99+
assert.isNaN(hsl("#000").s);
100+
assert.isNaN(hsl("black").s);
101+
assert.isNaN(hsl(_.rgb("black")).s);
102+
assert.isNaN(hsl("#fff").s);
103+
assert.isNaN(hsl("white").s);
104+
assert.isNaN(hsl(_.rgb("white")).s);
105+
},
106+
"can convert grayscale colors (with undefined hue) to RGB": function(hsl) {
107+
assert.strictEqual(hsl(NaN, 0, .2) + "", "#333333");
108+
assert.strictEqual(hsl(NaN, 0, .6) + "", "#999999");
109+
},
110+
"can convert white and black (with undefined hue and saturation) to RGB": function(hsl) {
111+
assert.strictEqual(hsl(NaN, NaN, 0) + "", "#000000");
112+
assert.strictEqual(hsl(NaN, NaN, 1) + "", "#ffffff");
75113
}
76114
}
77115
});

0 commit comments

Comments
 (0)