Skip to content

Instantly share code, notes, and snippets.

@diafygi
Last active August 29, 2015 14:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save diafygi/3ba625dab6127eb4b637 to your computer and use it in GitHub Desktop.
Save diafygi/3ba625dab6127eb4b637 to your computer and use it in GitHub Desktop.
d3 gradient heatmap
<!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