Last active
August 29, 2015 14:02
-
-
Save diafygi/3ba625dab6127eb4b637 to your computer and use it in GitHub Desktop.
d3 gradient heatmap
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> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | |
<style> | |
svg{ | |
display:inline-block; | |
} | |
</style> | |
</head> | |
<body> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script> | |
var w = 400, | |
h = 400; | |
var colors = [ | |
[0.00, "#00f"], | |
[0.33, "#0f0"], | |
[0.66, "#f00"], | |
[1.00, "#fff"], | |
]; | |
//calculate the heatmap paths | |
function gradient_paths(data){ | |
//get matrix dimensions | |
var len_x = data[0].length, | |
len_y = data.length; | |
//get min/max z values | |
var min_z, max_z; | |
for(var x = 0; x < len_x; x++){ | |
for(var y = 0; y < len_y; y++){ | |
//update min | |
if(min_z === undefined || min_z > data[y][x]){ | |
min_z = data[y][x]; | |
} | |
//update max | |
if(max_z === undefined || max_z < data[y][x]){ | |
max_z = data[y][x]; | |
} | |
} | |
} | |
//scale points | |
var X = d3.scale.linear() | |
.domain([0, len_x - 1]) | |
.range([0, w]); | |
var Y = d3.scale.linear() | |
.domain([0, len_y - 1]) | |
.range([0, h]); | |
//translate z values into colors | |
var z_domain = [], color_range = []; | |
for(var z = 0; z < colors.length; z++){ | |
z_domain.push((max_z - min_z) * colors[z][0] + min_z); | |
color_range.push(colors[z][1]); | |
} | |
var Z = d3.scale.linear() | |
.domain(z_domain) | |
.range(color_range); | |
//scale line | |
var line = d3.svg.line() | |
.x(function(d){ return d[0]; }) | |
.y(function(d){ return d[1]; }) | |
.interpolate("linear"); | |
//calculate stops for the gradients based on the color domain | |
function calc_stops(z1, z2){ | |
var z_list = []; | |
//no gradient, so just one stop | |
if(z1 === z2){ | |
z_list.push({"color": Z(z1), "offset": 1}); | |
} | |
//positive gradient | |
else if(z2 > z1){ | |
z_list.push({"color": Z(z1), "offset": 0}); | |
//add stops for all the color shifts | |
for(var i = 0; i < z_domain.length; i++){ | |
if(z_domain[i] <= z1 || z_domain[i] >= z2){ | |
continue; | |
} | |
z_list.push({ | |
"color": Z(z_domain[i]), | |
"offset": (z_domain[i] - z1) / (z2 - z1), | |
}); | |
} | |
z_list.push({"color": Z(z2), "offset": 1}); | |
} | |
//negative gradient | |
else if(z2 < z1){ | |
z_list.push({"color": Z(z1), "offset": 0}); | |
//add stops for all the color shifts | |
for(var i = z_domain.length - 1; i >= 0; i--){ | |
if(z_domain[i] >= z1 || z_domain[i] <= z2){ | |
continue; | |
} | |
z_list.push({ | |
"color": Z(z_domain[i]), | |
"offset": (z_domain[i] - z1) / (z2 - z1), | |
}); | |
} | |
z_list.push({"color": Z(z2), "offset": 1}); | |
} | |
return z_list; | |
} | |
//calculate gradients from three points | |
var id_counter = 0; | |
function calc_gradients(p1, p2, p3, no_scale){ | |
var result = []; | |
//transform to scale | |
if(!no_scale){ | |
p1 = [X(p1[0]), Y(p1[1]), p1[2]]; | |
p2 = [X(p2[0]), Y(p2[1]), p2[2]]; | |
p3 = [X(p3[0]), Y(p3[1]), p3[2]]; | |
} | |
//no gradient if all the points are the same z | |
if(p1[2] === p2[2] && p1[2] === p3[2]){ | |
result.push({ | |
"path": line([p1, p2, p3, p1]), | |
"id": "grad" + id_counter, | |
"start": [p1[0], p1[1]], | |
"end": [p1[0], p1[1]], | |
"stops": calc_stops(p1[2], p1[2]), | |
}); | |
} | |
//single gradient if two points are same z | |
else if(p1[2] === p2[2] || p1[2] === p3[2] || p2[2] === p3[2]){ | |
//make points z1 and z2 be along the same-z line | |
if(p1[2] === p2[2]){ | |
var z1 = p1; | |
var z2 = p2; | |
var z3 = p3; | |
} | |
else if(p1[2] === p3[2]){ | |
var z1 = p1; | |
var z2 = p3; | |
var z3 = p2; | |
} | |
else if(p2[2] === p3[2]){ | |
var z1 = p2; | |
var z2 = p3; | |
var z3 = p1; | |
} | |
//find line perpendicular to same-z line | |
//(vertical special case) | |
if(z2[0] - z1[0] < 0.00001 && z2[0] - z1[0] > -0.00001){ | |
var intersect_x = z1[0]; | |
var intersect_y = z3[1]; | |
} | |
//(horizontal special case) | |
else if(z2[1] - z1[1] < 0.00001 && z2[1] - z1[1] > -0.00001){ | |
var intersect_x = z3[0]; | |
var intersect_y = z1[1]; | |
} | |
//(normal case) | |
else{ | |
var m1 = (z2[1] - z1[1]) / (z2[0] - z1[0]); | |
//find point where same-z line intersects | |
var intersect_x = (z3[1] - z1[1] + (m1 * z1[0]) + (z3[0] / m1)) / (m1 + 1 / m1); | |
var intersect_y = (-1 / m1) * intersect_x + (z3[0] / m1) + z3[1]; | |
} | |
//make gradient follow the perpendiular line segment | |
result.push({ | |
"path": line([p1, p2, p3, p1]), | |
"id": "grad" + id_counter, | |
"start": [z3[0], z3[1]], | |
"end": [intersect_x, intersect_y], | |
"stops": calc_stops(z3[2], z1[2]), | |
}); | |
} | |
//split up triangle into two smaller triangles | |
//(so the linear gradients match up) | |
else{ | |
//sort points based on z value | |
var p123 = [p1, p2, p3].sort(function(a, b){ | |
if(a[2] < b[2]){ | |
return true; | |
} | |
return false; | |
}); | |
var z1 = p123[0], | |
z2 = p123[1], | |
z3 = p123[2]; | |
//determine location of z2 value on the z1-to-z3 line | |
//(vertical case) | |
if(z3[0] - z1[0] < 0.00001 && z3[0] - z1[0] > -0.00001){ | |
var m = (z3[2] - z1[2]) / (z3[1] - z1[1]); | |
var b = z1[2] - m * z1[1]; | |
var intersect_x = z1[0]; | |
var intersect_y = (z2[2] - b) / m; | |
} | |
//(horizontal case) | |
else if(z3[1] - z1[1] < 0.00001 && z3[1] - z1[1] > -0.00001){ | |
var m = (z3[2] - z1[2]) / (z3[0] - z1[0]); | |
var b = z1[2] - m * z1[0]; | |
var intersect_x = (z2[2] - b) / m; | |
var intersect_y = z1[1]; | |
} | |
//(normal case) | |
else{ | |
var intersect_x = (z3[0] - z1[0]) / (z3[2] - z1[2]) * (z2[2] - z3[2]) + z3[0]; | |
var intersect_y = (z3[1] - z1[1]) / (z3[2] - z1[2]) * (z2[2] - z3[2]) + z3[1]; | |
} | |
//get gradients for two smaller triangles | |
result = result.concat(calc_gradients(z1, z2, [intersect_x, intersect_y, z2[2]], true)); | |
result = result.concat(calc_gradients(z2, z3, [intersect_x, intersect_y, z2[2]], true)); | |
} | |
id_counter += 1; | |
return result; | |
} | |
//Go through grid and create four triangles for each segment | |
var result = []; | |
for(var x = 0; x < len_x - 1; x++){ | |
for(var y = 0; y < len_y - 1; y++){ | |
var avg = (data[y][x] + data[y+1][x] + data[y][x+1] + data[y+1][x+1]) / 4; | |
// \/ | |
result = result.concat(calc_gradients( | |
[x, y, data[y][x]], | |
[x+1, y, data[y][x+1]], | |
[x+0.5, y+0.5, avg])); | |
// |> | |
result = result.concat(calc_gradients( | |
[x, y, data[y][x]], | |
[x, y+1, data[y+1][x]], | |
[x+0.5, y+0.5, avg])); | |
// <| | |
result = result.concat(calc_gradients( | |
[x+1, y, data[y][x+1]], | |
[x+1, y+1, data[y+1][x+1]], | |
[x+0.5, y+0.5, avg])); | |
// /\ | |
result = result.concat(calc_gradients( | |
[x, y+1, data[y+1][x]], | |
[x+1, y+1, data[y+1][x+1]], | |
[x+0.5, y+0.5, avg])); | |
} | |
} | |
return result; | |
} | |
//insert the heatmap | |
function heatmap(data, location){ | |
var paths = gradient_paths(data); | |
//add the gradients | |
var grads = location.selectAll("linearGradient") | |
.data(paths) | |
.enter().append("linearGradient") | |
.attr("id", function(d){ return d.id; }) | |
.attr("x1", function(d){ return d.start[0]; }) | |
.attr("y1", function(d){ return d.start[1]; }) | |
.attr("x2", function(d){ return d.end[0]; }) | |
.attr("y2", function(d){ return d.end[1]; }) | |
.attr("gradientUnits", "userSpaceOnUse"); | |
//add the stops for the gradients | |
grads.selectAll("stop") | |
.data(function(d){ return d.stops; }) | |
.enter().append("stop") | |
.attr("offset", function(d){ return d.offset; }) | |
.attr("stop-color", function(d){ return d.color; }); | |
//add the triangles and associate them with their gradients | |
location.selectAll("path") | |
.data(paths) | |
.enter().append("path") | |
.attr("d", function(d){ return d.path; }) | |
.style("stroke-width", 1) | |
.style("stroke", function(d){ return "url(#" + d.id + ")"; }) | |
//.style("stroke", "#000") | |
.style("fill", function(d){ return "url(#" + d.id + ")"; }); | |
} | |
//manually enter some data | |
/* | |
var dataset = [ | |
[0, 0, 0, 0], | |
[0, 6, 4, 0], | |
[0, 4, 6, 0], | |
[0, 0, 0, 0], | |
]; | |
*/ | |
//generate some random data | |
/* | |
var dataset = [], | |
size = 20; | |
for(var i = 0; i < size; i++){ | |
var row = []; | |
for(var j = 0; j < size; j++){ | |
row.push(Math.random()); | |
} | |
dataset.push(row); | |
} | |
*/ | |
//generate a cosine plane | |
var dataset = [], | |
size = 20; | |
for(var i = 0; i < size; i++){ | |
var row = []; | |
for(var j = 0; j < size; j++){ | |
//z = cos(sqrt(x^2 + y^2)) | |
var x = i / size * Math.PI * 50, | |
y = j / size * Math.PI * 50; | |
row.push(Math.cos(Math.pow(Math.pow(x, 2) + Math.pow(y, 2), 0.5))); | |
} | |
dataset.push(row); | |
} | |
//TODO: interpolate based on chart size, not dataset size | |
var svg1 = d3.select("body").append("svg") | |
.attr("width", w) | |
.attr("height", h); | |
heatmap(dataset, svg1); | |
//compare the heatmap to a simple circle grid | |
function circle_grid(data, location){ | |
//calculate scales | |
var xyz = [],min_z, max_z; | |
for(var x = 0; x < data[0].length; x++){ | |
for(var y = 0; y < data.length; y++){ | |
xyz.push([x, y, data[y][x]]); | |
if(min_z === undefined || min_z > data[y][x]){ | |
min_z = data[y][x]; | |
} | |
if(max_z === undefined || max_z < data[y][x]){ | |
max_z = data[y][x]; | |
} | |
} | |
} | |
var X = d3.scale.linear() | |
.domain([0, data[0].length - 1]) | |
.range([0, w]); | |
var Y = d3.scale.linear() | |
.domain([0, data.length - 1]) | |
.range([0, h]); | |
var R = d3.scale.linear() | |
.domain([min_z, max_z]) | |
.range([50 / data.length, 180 / data.length]); | |
location.selectAll("circle") | |
.data(xyz) | |
.enter().append("circle") | |
.attr("cx", function(d){ return X(d[0]); }) | |
.attr("cy", function(d){ return Y(d[1]); }) | |
.attr("r", function(d){ return R(d[2]); }) | |
.attr("fill", "steelblue"); | |
} | |
var svg2 = d3.select("body").append("svg") | |
.attr("width", w) | |
.attr("height", h); | |
circle_grid(dataset, svg2); | |
//TODO: add a heatmap following the mouse | |
//svg.on("mousemove", function(){ | |
// var coords = d3.mouse(this); | |
//}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment