Skip to content

Commit

Permalink
Add d3.lab and d3.hcl.
Browse files Browse the repository at this point in the history
Includes d3.interpolateLab and d3.interpolateHcl.
  • Loading branch information
mbostock committed Aug 9, 2012
1 parent 952abef commit cded852
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 6 deletions.
3 changes: 3 additions & 0 deletions Makefile
Expand Up @@ -81,6 +81,9 @@ d3.core.js: \
src/core/uninterpolate.js \
src/core/rgb.js \
src/core/hsl.js \
src/core/hcl.js \
src/core/lab.js \
src/core/xyz.js \
src/core/selection.js \
src/core/selection-select.js \
src/core/selection-selectAll.js \
Expand Down
99 changes: 98 additions & 1 deletion d3.v2.js
Expand Up @@ -941,7 +941,24 @@
var h0 = a.h, s0 = a.s, l0 = a.l, h1 = b.h - h0, s1 = b.s - s0, l1 = b.l - l0;
if (h1 > 180) h1 -= 360; else if (h1 < -180) h1 += 360;
return function(t) {
return d3_hsl_rgb(h0 + h1 * t, s0 + s1 * t, l0 + l1 * t).toString();
return d3_hsl_rgb(h0 + h1 * t, s0 + s1 * t, l0 + l1 * t) + "";
};
};
d3.interpolateLab = function(a, b) {
a = d3.lab(a);
b = d3.lab(b);
var al = a.l, aa = a.a, ab = a.b, bl = b.l - al, ba = b.a - aa, bb = b.b - ab;
return function(t) {
return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + "";
};
};
d3.interpolateHcl = function(a, b) {
a = d3.hcl(a);
b = d3.hcl(b);
var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al;
if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;
return function(t) {
return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + "";
};
};
d3.interpolateArray = function(a, b) {
Expand Down Expand Up @@ -1078,6 +1095,16 @@
}
return d3_hsl(h, s, l);
}
function d3_rgb_lab(r, g, b) {
r = d3_rgb_xyz(r);
g = d3_rgb_xyz(g);
b = d3_rgb_xyz(b);
var x = d3_xyz_lab((.4124564 * r + .3575761 * g + .1804375 * b) / d3_lab_X), y = d3_xyz_lab((.2126729 * r + .7151522 * g + .072175 * b) / d3_lab_Y), z = d3_xyz_lab((.0193339 * r + .119192 * g + .9503041 * b) / d3_lab_Z);
return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z));
}
function d3_rgb_xyz(r) {
return (r /= 255) <= .04045 ? r / 12.92 : Math.pow((r + .055) / 1.055, 2.4);
}
function d3_rgb_parseNumber(c) {
var f = parseFloat(c);
return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f;
Expand Down Expand Up @@ -1279,6 +1306,76 @@
}
return d3_rgb(vv(h + 120), vv(h), vv(h - 120));
}
d3.hcl = function(h, c, l) {
return arguments.length === 1 ? h instanceof d3_Hcl ? d3_hcl(h.h, h.c, h.l) : h instanceof d3_Lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : d3_hcl(+h, +c, +l);
};
function d3_hcl(h, c, l) {
return new d3_Hcl(h, c, l);
}
function d3_Hcl(h, c, l) {
this.h = h;
this.c = c;
this.l = l;
}
d3_Hcl.prototype.brighter = function(k) {
return d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)));
};
d3_Hcl.prototype.darker = function(k) {
return d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)));
};
d3_Hcl.prototype.rgb = function() {
return d3_hcl_lab(this.h, this.c, this.l).rgb();
};
d3_Hcl.prototype.toString = function() {
return this.rgb() + "";
};
function d3_hcl_lab(h, c, l) {
return d3_lab(l, Math.cos(h *= Math.PI / 180) * c, Math.sin(h) * c);
}
d3.lab = function(l, a, b) {
return arguments.length === 1 ? l instanceof d3_Lab ? d3_lab(l.l, l.a, l.b) : l instanceof d3_Hcl ? d3_hcl_lab(l.l, l.c, l.h) : d3_rgb_lab((l = d3.rgb(l)).r, l.g, l.b) : d3_lab(+l, +a, +b);
};
function d3_lab(l, a, b) {
return new d3_Lab(l, a, b);
}
function d3_Lab(l, a, b) {
this.l = l;
this.a = a;
this.b = b;
}
var d3_lab_K = 18;
var d3_lab_X = .95047, d3_lab_Y = 1, d3_lab_Z = 1.08883;
d3_Lab.prototype.brighter = function(k) {
return d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
};
d3_Lab.prototype.darker = function(k) {
return d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
};
d3_Lab.prototype.rgb = function() {
return d3_lab_rgb(this.l, this.a, this.b);
};
d3_Lab.prototype.toString = function() {
return this.rgb() + "";
};
function d3_lab_rgb(l, a, b) {
var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200;
x = d3_lab_xyz(x) * d3_lab_X;
y = d3_lab_xyz(y) * d3_lab_Y;
z = d3_lab_xyz(z) * d3_lab_Z;
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));
}
function d3_lab_hcl(l, a, b) {
return d3_hcl(Math.atan2(b, a) / Math.PI * 180, Math.sqrt(a * a + b * b), l);
}
function d3_lab_xyz(x) {
return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037;
}
function d3_xyz_lab(x) {
return x > .008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29;
}
function d3_xyz_rgb(r) {
return Math.round(255 * (r <= .00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - .055));
}
function d3_selection(groups) {
d3_arraySubclass(groups, d3_selectionPrototype);
return groups;
Expand Down
8 changes: 4 additions & 4 deletions d3.v2.min.js

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions src/core/hcl.js
@@ -0,0 +1,37 @@
d3.hcl = function(h, c, l) {
return arguments.length === 1
? (h instanceof d3_Hcl ? d3_hcl(h.h, h.c, h.l)
: (h instanceof d3_Lab ? d3_lab_hcl(h.l, h.a, h.b)
: d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b)))
: d3_hcl(+h, +c, +l);
};

function d3_hcl(h, c, l) {
return new d3_Hcl(h, c, l);
}

function d3_Hcl(h, c, l) {
this.h = h;
this.c = c;
this.l = l;
}

d3_Hcl.prototype.brighter = function(k) {
return d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)));
};

d3_Hcl.prototype.darker = function(k) {
return d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)));
};

d3_Hcl.prototype.rgb = function() {
return d3_hcl_lab(this.h, this.c, this.l).rgb();
};

d3_Hcl.prototype.toString = function() {
return this.rgb() + "";
};

function d3_hcl_lab(h, c, l) {
return d3_lab(l, Math.cos(h *= Math.PI / 180) * c, Math.sin(h) * c);
}
31 changes: 30 additions & 1 deletion src/core/interpolate.js
Expand Up @@ -173,7 +173,36 @@ d3.interpolateHsl = function(a, b) {
l1 = b.l - l0;
if (h1 > 180) h1 -= 360; else if (h1 < -180) h1 += 360; // shortest path
return function(t) {
return d3_hsl_rgb(h0 + h1 * t, s0 + s1 * t, l0 + l1 * t).toString();
return d3_hsl_rgb(h0 + h1 * t, s0 + s1 * t, l0 + l1 * t) + "";
};
};

d3.interpolateLab = function(a, b) {
a = d3.lab(a);
b = d3.lab(b);
var al = a.l,
aa = a.a,
ab = a.b,
bl = b.l - al,
ba = b.a - aa,
bb = b.b - ab;
return function(t) {
return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + "";
};
};

d3.interpolateHcl = function(a, b) {
a = d3.hcl(a);
b = d3.hcl(b);
var ah = a.h,
ac = a.c,
al = a.l,
bh = b.h - ah,
bc = b.c - ac,
bl = b.l - al;
if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; // shortest path
return function(t) {
return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + "";
};
};

Expand Down
63 changes: 63 additions & 0 deletions src/core/lab.js
@@ -0,0 +1,63 @@
d3.lab = function(l, a, b) {
return arguments.length === 1
? (l instanceof d3_Lab ? d3_lab(l.l, l.a, l.b)
: (l instanceof d3_Hcl ? d3_hcl_lab(l.l, l.c, l.h)
: d3_rgb_lab((l = d3.rgb(l)).r, l.g, l.b)))
: d3_lab(+l, +a, +b);
};

function d3_lab(l, a, b) {
return new d3_Lab(l, a, b);
}

function d3_Lab(l, a, b) {
this.l = l;
this.a = a;
this.b = b;
}

// Corresponds roughly to RGB brighter/darker
var d3_lab_K = 18;

// D65 standard referent
var d3_lab_X = 0.950470,
d3_lab_Y = 1,
d3_lab_Z = 1.088830;

d3_Lab.prototype.brighter = function(k) {
return d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
};

d3_Lab.prototype.darker = function(k) {
return d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
};

d3_Lab.prototype.rgb = function() {
return d3_lab_rgb(this.l, this.a, this.b);
};

d3_Lab.prototype.toString = function() {
return this.rgb() + "";
};

function d3_lab_rgb(l, a, b) {
var y = (l + 16) / 116,
x = y + a / 500,
z = y - b / 200;
x = d3_lab_xyz(x) * d3_lab_X;
y = d3_lab_xyz(y) * d3_lab_Y;
z = d3_lab_xyz(z) * d3_lab_Z;
return d3_rgb(
d3_xyz_rgb( 3.2404542 * x - 1.5371385 * y - 0.4985314 * z),
d3_xyz_rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z),
d3_xyz_rgb( 0.0556434 * x - 0.2040259 * y + 1.0572252 * z)
);
}

function d3_lab_hcl(l, a, b) {
return d3_hcl(Math.atan2(b, a) / Math.PI * 180, Math.sqrt(a * a + b * b), l);
}

function d3_lab_xyz(x) {
return x > 0.206893034 ? x * x * x : (x - 4 / 29) / 7.787037;
}
14 changes: 14 additions & 0 deletions src/core/rgb.js
Expand Up @@ -124,6 +124,20 @@ function d3_rgb_hsl(r, g, b) {
return d3_hsl(h, s, l);
}

function d3_rgb_lab(r, g, b) {
r = d3_rgb_xyz(r);
g = d3_rgb_xyz(g);
b = d3_rgb_xyz(b);
var x = d3_xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / d3_lab_X),
y = d3_xyz_lab((0.2126729 * r + 0.7151522 * g + 0.0721750 * b) / d3_lab_Y),
z = d3_xyz_lab((0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / d3_lab_Z);
return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z));
}

function d3_rgb_xyz(r) {
return (r /= 255) <= 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
}

function d3_rgb_parseNumber(c) { // either integer or percentage
var f = parseFloat(c);
return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f;
Expand Down
7 changes: 7 additions & 0 deletions src/core/xyz.js
@@ -0,0 +1,7 @@
function d3_xyz_lab(x) {
return x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29;
}

function d3_xyz_rgb(r) {
return Math.round(255 * (r <= 0.00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - 0.055));
}
89 changes: 89 additions & 0 deletions test/core/hcl-test.js
@@ -0,0 +1,89 @@
require("../env");

var vows = require("vows"),
assert = require("assert");

var suite = vows.describe("d3.hcl");

suite.addBatch({
"hcl": {
topic: function() {
return d3.hcl;
},
"converts string channel values to numbers": function(hcl) {
assertHclEqual(hcl("50", "-4", "32"), 50, -4, 32);
},
"converts null channel values to zero": function(hcl) {
assertHclEqual(hcl(null, null, null), 0, 0, 0);
},
"exposes h, c and l properties": function(hcl) {
var color = hcl(50, -4, 32);
assert.equal(color.h, 50);
assert.equal(color.c, -4);
assert.equal(color.l, 32);
},
"changing h, c or l affects the string format": function(hcl) {
var color = hcl(50, -4, 32);
assert.equal(color + "", "#444d50");
color.h++;
assert.equal(color + "", "#444d50");
color.c++;
assert.equal(color + "", "#464c4f");
color.l++;
assert.equal(color + "", "#494f51");
},
"parses hexadecimal shorthand format (e.g., \"#abc\")": function(hcl) {
assertHclEqual(hcl("#abc"), -102.28223831811077, 10.774886733325554, 75.10497524893663);
},
"parses hexadecimal format (e.g., \"#abcdef\")": function(hcl) {
assertHclEqual(hcl("#abcdef"), -100.15785184209284, 20.768234621934273, 81.04386565274363);
},
"parses HSL format (e.g., \"hsl(210, 64%, 13%)\")": function(hcl) {
assertHclEqual(hcl("hsl(210, 64.7058%, 13.33333%)"), -89.58282792342067, 16.833655998102003, 12.65624852526134);
},
"parses color names (e.g., \"moccasin\")": function(hcl) {
assertHclEqual(hcl("moccasin"), 84.71288921124494, 26.472460854104156, 91.72317744746022);
},
"parses and converts RGB format (e.g., \"rgb(102, 102, 0)\")": function(hcl) {
assertHclEqual(hcl("rgb(102, 102, 0)"), 102.85124420310271, 49.44871600399321, 41.73251953866431);
},
"can convert from RGB": function(hcl) {
assertHclEqual(hcl(d3.rgb(12, 34, 56)), -89.58282792342067, 16.833655998102003, 12.65624852526134);
},
"can convert from HSL": function(hcl) {
assertHclEqual(hcl(d3.hcl(20, .8, .3)), 20, 0.8, 0.3);
},
"can convert to RGB": function(hcl) {
assert.rgbEqual(hcl("steelblue").rgb(), 70, 130, 180);
},
"can derive a brighter color": function(hcl) {
assertHclEqual(hcl("steelblue").brighter(), -97.21873224090723, 32.44906314974561, 70.46551718768575);
assertHclEqual(hcl("steelblue").brighter(.5), -97.21873224090723, 32.44906314974561, 61.46551718768575);
},
"can derive a darker color": function(hcl) {
assertHclEqual(hcl("lightsteelblue").darker(), -94.8160116310511, 15.26488988314746, 60.45157936968134);
assertHclEqual(hcl("lightsteelblue").darker(.5), -94.8160116310511, 15.26488988314746, 69.45157936968134);
},
"string coercion returns RGB format": function(hcl) {
assert.strictEqual(hcl("hsl(60, 100%, 20%)") + "", "#666600");
assert.strictEqual(hcl(d3.hcl(60, -4, 32)) + "", "#454c51");
},
"roundtrip to HSL is idempotent": function(hcl) {
assert.hslEqual(d3.hsl(hcl("steelblue")), d3.hsl("steelblue"));
},
"roundtrip to RGB is idempotent": function(hcl) {
assert.hslEqual(d3.rgb(hcl("steelblue")), d3.rgb("steelblue"));
},
"roundtrip to Lab is idempotent": function(hcl) {
assert.hslEqual(d3.lab(hcl("steelblue")), d3.lab("steelblue"));
}
}
});

suite.export(module);

function assertHclEqual(actual, h, c, l, message) {
if (Math.abs(actual.h - h) > 1e-6 || Math.abs(actual.c - c) > 1e-6 || Math.abs(actual.l - l) > 1e-6) {
assert.fail("hcl(" + actual.h + ", " + actual.c + ", " + actual.l + ")", "hcl(" + h + ", " + c + ", " + l + ")", message || "expected {expected}, got {actual}", null, assertHclEqual);
}
}

0 comments on commit cded852

Please sign in to comment.