A 3-series histogram of red, green, and blue hue luminance distribution. The bars are z-axis sorted by height, and the graph is sortable by color, with animation.
z-stacked multi-series bar chart w animation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<link href="stack.css" type="text/css" rel="stylesheet"/> | |
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<body> | |
<section id="rgb-luminance"> | |
<ul> | |
<li> | |
<button class="rgb" type="button">All</button> | |
</li> | |
<li> | |
<button class="red" type="button">Red</button> | |
</li> | |
<li> | |
<button class="green" type="button">Green</button> | |
</li> | |
<li> | |
<button class="blue" type="button">Blue</button> | |
</li> | |
</ul> | |
</section> | |
</body> | |
<script> | |
//helpers | |
function rgb2xyz (color) { | |
color = [color[0]/255, color[1]/255, color[2]/255] | |
var xyz = [0, 0, 0], | |
matrix = [[0.412453, 0.357580, 0.180423], | |
[0.212671, 0.715160, 0.072169], | |
[0.019334, 0.119193, 0.950227]] | |
matrix.forEach(function(row, i) { | |
row.forEach(function(cell, j) { | |
xyz[i] += color[j] * cell * 100 | |
}) | |
}) | |
return xyz | |
} | |
function xyz2Lab (color) { | |
var d65 = [95.0456, 100, 108.8754], | |
xyzN = [], | |
L, a, b | |
//find X/Xn, Y/Yn, Z/Zn | |
color.forEach(function(d, i) { | |
xyzN.push(d/d65[i]) | |
}) | |
var f = function(t) { | |
if (t > 0.008856) | |
return Math.pow(t, 1/3) | |
else | |
return 7.787 * t + 16/116 | |
} | |
if (xyzN[1] > 0.008856) | |
L = 116 * Math.pow(xyzN[1], 1/3) - 16 | |
else | |
L = 903.3 * xyzN[1] | |
a = 500 * ( f(xyzN[0]) - f(xyzN[1]) ) | |
b = 200 * ( f(xyzN[1]) - f(xyzN[2]) ) | |
return [L, a, b] | |
} | |
function rgb2Lab(color) { | |
return xyz2Lab(rgb2xyz(color)) | |
} | |
function lumValsByPrime(prime) { | |
var results = [], | |
colors = { | |
r: function(r, i) { return [r, i, i] }, | |
g: function(g, i) { return [i, g, i] }, | |
b: function(b, i) { return [i, i, b] } | |
} | |
prime = prime.toLowerCase() | |
for (var p = 0; p < 256; p++) { | |
for (var i = 0; i < 256; i++) { | |
var luminance = Math.floor( rgb2Lab( colors[prime](p, i) )[0] ) | |
results.push(luminance) | |
} | |
} | |
return results | |
} | |
//draw visualization | |
//config | |
var vis = { | |
'container': '#rgb-luminance', | |
'title': ['Histogram of Red, Green, and Blue Hue Luminance Values'], | |
'subtitle': 'in thousands of colors' | |
} | |
//calculated config | |
vis.svg = { | |
width: d3.select(vis.container).node().clientWidth, | |
height: d3.select(vis.container).node().clientHeight | |
} | |
vis.pad = {top: 80, right: 40, bottom: 40, left: 40} | |
vis.width = vis.svg.width - vis.pad.left - vis.pad.right | |
vis.height = vis.svg.height - vis.pad.top - vis.pad.bottom | |
vis.colors = { | |
r: function(r, i) { return 'rgb('+r+','+i+','+i+')' }, | |
g: function(g, i) { return 'rgb('+i+','+g+','+i+')' }, | |
b: function(b, i) { return 'rgb('+i+','+i+','+b+')' } | |
} | |
var x = d3.scale.linear() | |
.domain([0, 100]) | |
.range([0, vis.width]) | |
var histR = d3.layout.histogram() | |
.bins(x.ticks(100)) | |
(lumValsByPrime('r')) | |
var histG = d3.layout.histogram() | |
.bins(x.ticks(100)) | |
(lumValsByPrime('g')) | |
var histB = d3.layout.histogram() | |
.bins(x.ticks(100)) | |
(lumValsByPrime('b')) | |
var y = d3.scale.linear() | |
.domain([0, 2000]) | |
.range([vis.height, 0]) | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient('bottom') | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.ticks(4) | |
.tickFormat(function(d) { return d / 1000 }) | |
.orient('left') | |
var svg = d3.select(vis.container).append('svg') | |
.attr('width', vis.svg.width) | |
.attr('height', vis.svg.height) | |
var title = svg.append('text') | |
.attr('class', 'title') | |
.attr('text-anchor', 'middle') | |
.attr('transform', 'translate(' + | |
vis.svg.width / 2 + ',' + vis.pad.top / 2 + ')') | |
.text(vis.title) | |
.append('tspan') | |
.attr('x', 0) | |
.attr('dy', 1.1+'em') | |
.text(vis.subtitle) | |
var graph = svg.append('g') | |
.attr('class', 'graph') | |
.attr('width', vis.width) | |
.attr('height', vis.height) | |
.attr('transform', 'translate('+vis.pad.left+','+vis.pad.top+')') | |
//sort and combine all three histograms so shortest bar is in front | |
var sortColors = function() { | |
var result = [] | |
var colorOrder = function(d, i) { | |
var red = d, | |
green = histG[i], | |
blue = histB[i] | |
//mark color, for fill function | |
red.c = 'r' | |
green.c = 'g' | |
blue.c = 'b' | |
//sort largest (in back) to smallest (up front) | |
var order = [red, green, blue].sort(function (a, b) { | |
return b.y - a.y; | |
}) | |
order.forEach(function(col) { | |
result.push(col) | |
}) | |
} | |
histR.forEach(function(d, i) { | |
colorOrder(d,i) | |
}) | |
return result | |
} | |
var histogram = sortColors() | |
var bar = graph.selectAll('.bar') | |
.data(histogram) | |
.enter().append('g') | |
.attr('class', 'bar') | |
.attr('transform', function(d) { | |
return 'translate('+x(d.x)+',0)'//,'+y(d.y)+')' | |
}) | |
bar.append('rect') | |
.attr('x', 1) | |
.attr('y', function(d) { return y(d.y) }) | |
.attr('width', x(histR[0].dx) - 1) | |
.attr('height', function(d) { | |
return vis.height - y(d.y) | |
}) | |
.attr('fill', function(d) { | |
//map colors 0 - 150 (for legibility) | |
var n = Math.round(d.x * 1.5) | |
return vis.colors[d.c](255, n)//'rgb(255,'+n+','+n+')' | |
}) | |
graph.append('g') | |
.attr('class', 'x axis') | |
.attr('transform', 'translate(0,' + vis.height + ')') | |
.call(xAxis) | |
graph.append('g') | |
.attr('class', 'y axis') | |
.call(yAxis) | |
//hide colors on button click | |
var visNode = d3.select('#rgb-luminance'), | |
rects = visNode.selectAll('rect'), | |
btns = ['rgb', 'red', 'green', 'blue'] | |
btns.forEach(function(btnColor) { | |
visNode.select('.'+btnColor).on('click', function() { | |
rects.transition() | |
.duration(500) | |
.attr('height', function(d) { | |
if (d.c !== btnColor.substr(0,1) && btnColor !== 'rgb') | |
return 0 | |
else | |
return vis.height - y(d.y) | |
}) | |
.attr('y', function(d) { | |
if (d.c !== btnColor.substr(0,1) && btnColor !== 'rgb') | |
return vis.height | |
else | |
return y(d.y) | |
}) | |
}) | |
}) | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
font-family: helvetica, sans-serif; | |
font-size: 14px; | |
} | |
#rgb-luminance { | |
width: 100vw; | |
height: 100vh; | |
} | |
text { fill: black; } | |
line, | |
path { | |
fill: transparent; | |
stroke-width: 2; | |
stroke: black; | |
} | |
text.title { font-size: 18px; } | |
.title tspan { font-size: 14px; } | |
.bar rect { | |
stroke-width: 1; | |
} | |
.axis line, | |
.domain { | |
stroke-width: 2; | |
stroke: gray; | |
} | |
ul { | |
list-style: none; | |
position: absolute; | |
top: 15%; | |
left: 4%; | |
} | |
button { | |
width: 60px; | |
height: 26px; | |
margin-bottom: 2px; | |
border: none; | |
border-radius: 4px; | |
background-color: #3e3e3e; | |
color: white; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment