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 LAB color space support. #183

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ all: \
d3.chart.min.js \
d3.layout.js \
d3.layout.min.js \
d3.color.js \
d3.color.min.js \
d3.csv.js \
d3.csv.min.js \
d3.geo.js \
Expand Down Expand Up @@ -69,6 +71,9 @@ d3.core.js: \
src/core/transition.js \
src/core/timer.js

d3.color.js: \
src/color/lab.js

d3.scale.js: \
src/scale/scale.js \
src/scale/linear.js \
Expand Down Expand Up @@ -177,6 +182,7 @@ tests: \
tests/test-rgb.test \
tests/test-round.test \
tests/test-hsl.test \
tests/test-color-lab.test \
tests/test-time-format.test \
tests/test-time-parse.test \
tests/test-transition.test \
Expand Down
201 changes: 201 additions & 0 deletions d3.color.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// CIE L*a*b* Color Space and Difference Metrics
// Uses formulae and matrices from http://brucelindbloom.com/

d3.lab = function(L, a, b) {
if (arguments.length == 1) {
var rgb = d3.rgb(L);
return d3_rgb_lab(rgb.r, rgb.g, rgb.b);
} else {
return 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;
}

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

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

d3_Lab.prototype.distance = function(c) {
return d3_cie76(this, c);
}

d3_Lab.prototype.de76 = function(c) {
return d3_cie76(this, c);
}

d3_Lab.prototype.de94 = function(c) {
return d3_cie94(this, c);
}

d3_Lab.prototype.de00 = function(c) {
return d3_ciede2000(this, c);
}

function d3_lab_rgb(L, a, b) {
// first, map CIE L*a*b* to CIE XYZ
var y = (L + 16) / 116,
x = y + a/500,
z = y - b/200;

// D65 standard referent
var X = 0.950470, Y = 1.0, Z = 1.088830;

x = X * (x > 0.206893034 ? x*x*x : (x - 4.0/29) / 7.787037);
y = Y * (y > 0.206893034 ? y*y*y : (y - 4.0/29) / 7.787037);
z = Z * (z > 0.206893034 ? z*z*z : (z - 4.0/29) / 7.787037);

// second, map CIE XYZ to sRGB
var r = 3.2404542*x - 1.5371385*y - 0.4985314*z,
g = -0.9692660*x + 1.8760108*y + 0.0415560*z,
b = 0.0556434*x - 0.2040259*y + 1.0572252*z;
r = r <= 0.00304 ? 12.92*r : 1.055*Math.pow(r,1/2.4) - 0.055,
g = g <= 0.00304 ? 12.92*g : 1.055*Math.pow(g,1/2.4) - 0.055,
b = b <= 0.00304 ? 12.92*b : 1.055*Math.pow(b,1/2.4) - 0.055;

// third, discretize and return RGB values
r = Math.round(255*r);
g = Math.round(255*g);
b = Math.round(255*b);
var c = d3.rgb(
Math.max(0, Math.min(255, r)),
Math.max(0, Math.min(255, g)),
Math.max(0, Math.min(255, b))
);
if (r<0 || r>255 || g<0 || g>255 || b<0 || b>255)
c.clipped = true; // out of RGB gamut
return c;
}

function d3_rgb_lab(r, g, b) {
// first, normalize RGB values
r = r / 255.0;
g = g / 255.0;
b = b / 255.0;

// D65 standard referent
var X = 0.950470, Y = 1.0, Z = 1.088830;

// second, map sRGB to CIE XYZ
r = r <= 0.04045 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
g = g <= 0.04045 ? g/12.92 : Math.pow((g+0.055)/1.055, 2.4);
b = b <= 0.04045 ? b/12.92 : Math.pow((b+0.055)/1.055, 2.4);
var x = (0.4124564*r + 0.3575761*g + 0.1804375*b) / X,
y = (0.2126729*r + 0.7151522*g + 0.0721750*b) / Y,
z = (0.0193339*r + 0.1191920*g + 0.9503041*b) / Z;

// third, map CIE XYZ to CIE L*a*b* and return
x = x > 0.008856 ? Math.pow(x, 1/3) : 7.787037*x + 4.0/29;
y = y > 0.008856 ? Math.pow(y, 1/3) : 7.787037*y + 4.0/29;
z = z > 0.008856 ? Math.pow(z, 1/3) : 7.787037*z + 4.0/29;

return d3_lab(116*y - 16, 500*(x-y), 200*(y-z));
}

function d3_cie76(x, y) {
// distance of ~= 2.3 corresponds to one JND
var dL = x.L - y.L,
da = x.a - y.a,
db = x.b - y.b;
return Math.sqrt(dL*dL + da*da + db*db);
}

function d3_cie94(x, y) {
// uses constants for graphic arts (1, 0.045, 0.015)
// NOT textiles (2, 0.048, 0.014)
var dL = x.L - y.L,
da = x.a - y.a,
db = x.b - y.b,
C1 = Math.sqrt(x.a*x.a + x.b*x.b),
C2 = Math.sqrt(y.a*y.a + y.b*y.b),
dC = C1 - C2,
dH = Math.sqrt(da*da + db*db - dC*dC);
dC = dC / (1 + 0.045*C1);
dH = dH / (1 + 0.015*C1);
return Math.sqrt(dL*dL + dC*dC + dH*dH);
}

function d3_ciede2000(x, y) {
// adapted from Sharma et al's MATLAB implementation at
// http://www.ece.rochester.edu/~gsharma/ciede2000/

// parametric factors, use defaults
var kl = 1, kc = 1, kh = 1;

// compute terms
var pi = Math.PI,
L1 = x.L, a1 = x.a, b1 = x.b, Cab1 = Math.sqrt(a1*a1 + b1*b1),
L2 = y.L, a2 = y.a, b2 = y.b, Cab2 = Math.sqrt(a2*a2 + b2*b2),
Cab = 0.5*(Cab1 + Cab2),
G = 0.5*(1 - Math.sqrt(Math.pow(Cab,7)/(Math.pow(Cab,7)+Math.pow(25,7)))),
ap1 = (1+G) * a1,
ap2 = (1+G) * a2,
Cp1 = Math.sqrt(ap1*ap1 + b1*b1),
Cp2 = Math.sqrt(ap2*ap2 + b2*b2),
Cpp = Cp1 * Cp2;

// ensure hue is between 0 and 2pi
var hp1 = Math.atan2(b1, ap1); if (hp1 < 0) hp1 += 2*pi;
var hp2 = Math.atan2(b2, ap2); if (hp2 < 0) hp2 += 2*pi;

var dL = L2 - L1,
dC = Cp2 - Cp1,
dhp = hp2 - hp1;

if (dhp > +pi) dhp -= 2*pi;
if (dhp < -pi) dhp += 2*pi;
if (Cpp == 0) dhp = 0;

// Note that the defining equations actually need
// signed Hue and chroma differences which is different
// from prior color difference formulae
var dH = 2 * Math.sqrt(Cpp) * Math.sin(dhp/2);

// Weighting functions
var Lp = 0.5 * (L1 + L2),
Cp = 0.5 * (Cp1 + Cp2);

// Average Hue Computation
// This is equivalent to that in the paper but simpler programmatically.
// Average hue is computed in radians and converted to degrees where needed
var hp = 0.5 * (hp1 + hp2);
// Identify positions for which abs hue diff exceeds 180 degrees
if (Math.abs(hp1-hp2) > pi) hp -= pi;
if (hp < 0) hp += 2*pi;

// Check if one of the chroma values is zero, in which case set
// mean hue to the sum which is equivalent to other value
if (Cpp == 0) hp = hp1 + hp2;

var Lpm502 = (Lp-50) * (Lp-50),
Sl = 1 + 0.015*Lpm502 / Math.sqrt(20+Lpm502),
Sc = 1 + 0.045*Cp,
T = 1 - 0.17*Math.cos(hp - pi/6)
+ 0.24*Math.cos(2*hp)
+ 0.32*Math.cos(3*hp+pi/30)
- 0.20*Math.cos(4*hp - 63*pi/180),
Sh = 1 + 0.015 * Cp * T,
ex = (180/pi*hp-275) / 25,
delthetarad = (30*pi/180) * Math.exp(-1 * (ex*ex)),
Rc = 2 * Math.sqrt(Math.pow(Cp,7) / (Math.pow(Cp,7) + Math.pow(25,7))),
RT = -1 * Math.sin(2*delthetarad) * Rc;

dL = dL / (kl*Sl);
dC = dC / (kc*Sc);
dH = dH / (kh*Sh);

// The CIE 00 color difference
return Math.sqrt(dL*dL + dC*dC + dH*dH + RT * dC * dH);
}
3 changes: 3 additions & 0 deletions d3.color.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions examples/colorspace/colorspace.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>CIELAB Color Space</title>
<script type="text/javascript" src="../../d3.js"></script>
<script type="text/javascript" src="../../d3.color.js"></script>
<style type="text/css">
* { font-family: Helvetica Neue, Helvetica, Arial, sans-serif; }
div.section {
display: inline-block;
padding-right: 15px;
padding-bottom: 15px;
}
.header { margin-bottom: 20px; }
.title { font-size: 18px; font-weight: bold; }
.selector { margin-left: 20px; }
</style>
</head>
<body>
<div class="header">
<span class="title">L*a*b* Color Space, Clipped to the RGB Gamut</span>
<span class="selector">Step size (in a*, b*): &nbsp;
<select id="select"></select>
</span>
</div>
<div id="colors">
</div>
<script type="text/javascript" src="colorspace.js"></script>
</body>
</html>
82 changes: 82 additions & 0 deletions examples/colorspace/colorspace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
var L = [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 99],
query = location.search.slice(1),
step = query ? Math.max(2, ~~query) : 5,
size = 150, span, a, b;

function cross(a) {
return function(d) {
var c = [];
for (var i = 0, n = a.length; i < n; i++)
c.push([d, a[i]]);
return c;
};
}
function labcross(b) {
return function(d) {
var c = [];
for (var i = 0, n = b.length; i < n; i++)
c.push(d3.lab(d[0], d[1], b[i]));
return c;
};
}

// Populate step size selector widget
d3.select("#select")
.on("change", function() {
update(this.options[this.selectedIndex].value)
})
.selectAll("option")
.data([2,3,4,5,10,20,25,50])
.enter().append("option")
.property("value", function(d) { return d; })
.property("selected", function(d) { return d==step; })
.text(function(d) { return d.toFixed(0); });

// Visualize the color space
update(step);

// Generate L*a*b* color space cross-sections
function update(res) {
step = res;
span = ~~(size / (200 / step) + 0.5);
a = d3.range(-100,+101, step),
b = d3.range(+100,-101,-1*step);

// Remove any previous instantiations
d3.select("body").selectAll("div.section").remove();

// One div per L* cross-section
var div = d3.select("body").selectAll("div.section")
.data(L)
.enter().append("div")
.attr("class", "section");

// Label the L* value
div.append("div")
.text(function(L) { return "L* = " + L; });

// Add SVG for cross-section
var svg = div.append("svg:svg")
.attr("width", span * a.length)
.attr("height", span * b.length);

// One column per a* value.
var column = svg.selectAll("g")
.data(cross(a))
.enter().append("svg:g")
.attr("transform", function(d, i) { return "translate(" + i*span + ",0)"; });

// One row per b* value.
column.selectAll("rect")
.data(labcross(b))
.enter().append("svg:rect")
.attr("transform", function(c, i) { return "translate(0," + i*span + ")"; })
.attr("width", span)
.attr("height", span)
.style("fill", function(c) { return c.rgb().clipped ? "#808080" : c; })
.append("svg:title")
.text(function(c) {
return [c.L,c.a,c.b].map(function(d) { return d.toFixed(1); }).join(", ")
+ " -> " + c + (c.rgb().clipped ? " (clipped)" : "");
});
}
9 changes: 9 additions & 0 deletions examples/colorspace/section.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import url("../../lib/jquery-ui/jquery-ui.css");

* { font-family: Helvetica Neue, Helvetica, Arial, sans-serif; }

body, .ui-widget { font-size: 14px; }

#Llabel { margin-bottom: 6px; }

div { width: 500px; }
32 changes: 32 additions & 0 deletions examples/colorspace/section.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>CIELAB Cross Section Viewer</title>
<script type="text/javascript" src="../../d3.js"></script>
<script type="text/javascript" src="../../d3.color.js"></script>
<script type="text/javascript" src="../../lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="../../lib/jquery-ui/jquery-ui.min.js"></script>
<link type="text/css" rel="stylesheet" href="section.css"/>
</head>
<body>
<h3>CIE L*a*b* Cross-Section Viewer</h3>
<script type="text/javascript" src="section.js"></script><p>
<div id="Llabel">L* = <span>50</span></div>
<div id="L"></div>

<script type="text/javascript">
$("#L").slider({
min: 0,
max: 100,
step: 1,
value: 50,
slide: function(event, ui) {
d3.select("#Llabel span").text(ui.value);
L = ui.value;
refresh();
}
});
</script>
</body>
</html>
Loading