A fancy (and not that useful) visualization of random data in two dimensions as an isometric bar chart. Notice how the phenomenon of occlusion makes part of the dataset invisible.
forked from nitaku's block: Isometric projection
license: mit |
A fancy (and not that useful) visualization of random data in two dimensions as an isometric bar chart. Notice how the phenomenon of occlusion makes part of the dataset invisible.
forked from nitaku's block: Isometric projection
svg = d3.select('svg') | |
width = svg.node().getBoundingClientRect().width | |
height = svg.node().getBoundingClientRect().height | |
vis = svg.append('g') | |
.attr | |
transform: "translate(#{width/2},#{height/2})" | |
# [x, y, z] -> [-Math.sqrt(3)/2*x+Math.sqrt(3)/2*y, 0.5*x+0.5*y-z] | |
isometric = (_3d_p) -> [-Math.sqrt(3)/2*_3d_p[0]+Math.sqrt(3)/2*_3d_p[1], +0.5*_3d_p[0]+0.5*_3d_p[1]-_3d_p[2]] | |
parallelepipedon = (d) -> | |
d.x = 0 if not d.x? | |
d.y = 0 if not d.y? | |
d.z = 0 if not d.z? | |
d.dx = 10 if not d.dx? | |
d.dy = 10 if not d.dy? | |
d.dz = 10 if not d.dz? | |
fb = isometric [d.x, d.y, d.z], | |
mlb = isometric [d.x+d.dx, d.y, d.z], | |
nb = isometric [d.x+d.dx, d.y+d.dy, d.z], | |
mrb = isometric [d.x, d.y+d.dy, d.z], | |
ft = isometric [d.x, d.y, d.z+d.dz], | |
mlt = isometric [d.x+d.dx, d.y, d.z+d.dz], | |
nt = isometric [d.x+d.dx, d.y+d.dy, d.z+d.dz], | |
mrt = isometric [d.x, d.y+d.dy, d.z+d.dz] | |
d.iso = { | |
face_bottom: [fb, mrb, nb, mlb], | |
face_left: [mlb, mlt, nt, nb], | |
face_right: [nt, mrt, mrb, nb], | |
face_top: [ft, mrt, nt, mlt], | |
outline: [ft, mrt, mrb, nb, mlb, mlt] | |
far_point: fb # used to control the z-index of iso objects | |
} | |
return d | |
iso_layout = (data, shape, scale) -> | |
scale = 1 if not scale? | |
data.forEach (d) -> | |
shape(d, scale) | |
# data must be drawn from farthest to nearest | |
data.sort (a,b) -> a.iso.far_point[1] - b.iso.far_point[1] | |
path_generator = (d) -> 'M' + d.map((p)->p.join(' ')).join('L') + 'z' | |
y_color = d3.scale.category10() | |
L = 30 | |
PAD = 6 | |
data = d3.range(6*6).map (i) -> { | |
x: (i%6)*L, | |
y: Math.floor(i/6)*L, | |
dx: L-PAD, | |
dy: L-PAD, | |
dz: 10+Math.random()*6*L | |
} | |
iso_layout(data, parallelepipedon) | |
pipedons = vis.selectAll('.pipedon') | |
.data(data) | |
enter_pipedons = pipedons.enter().append('g') | |
.attr | |
class: 'pipedon' | |
enter_pipedons.append('path') | |
.attr | |
class: 'iso face bottom' | |
d: (d) -> path_generator(d.iso.face_bottom) | |
enter_pipedons.append('path') | |
.attr | |
class: 'iso face left template' | |
d: (d) -> path_generator(d.iso.face_left) | |
fill: (d) -> | |
# color the template face according to y | |
return y_color(d.y) | |
enter_pipedons.append('path') | |
.attr | |
class: 'iso face right' | |
d: (d) -> path_generator(d.iso.face_right) | |
fill: (d) -> | |
# right face is darker than the template (left face) | |
color = d3.hcl(d3.select(this.parentNode).select('.template').style('fill')) | |
return d3.hcl(color.h, color.c, color.l-12) | |
enter_pipedons.append('path') | |
.attr | |
class: 'iso face top' | |
d: (d) -> path_generator(d.iso.face_top) | |
fill: (d) -> | |
# right face is brighter than the template (left face) | |
color = d3.hcl(d3.select(this.parentNode).select('.template').style('fill')) | |
return d3.hcl(color.h, color.c, color.l+12) | |
enter_pipedons.append('path') | |
.attr | |
class: 'iso outline' | |
d: (d) -> path_generator(d.iso.outline) |
.iso.face.bottom { | |
display: none; | |
} | |
.iso.outline { | |
stroke: white; | |
fill: none; | |
} |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Isometric Projection</title> | |
<link type="text/css" href="index.css" rel="stylesheet"/> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
</head> | |
<body> | |
<svg width="960px" height="500px"></svg> | |
<script src="index.js"></script> | |
</body> | |
</html> |
// Generated by CoffeeScript 1.4.0 | |
(function() { | |
var L, PAD, data, enter_pipedons, height, iso_layout, isometric, parallelepipedon, path_generator, pipedons, svg, vis, width, y_color; | |
svg = d3.select('svg'); | |
width = svg.node().getBoundingClientRect().width; | |
height = svg.node().getBoundingClientRect().height; | |
vis = svg.append('g').attr({ | |
transform: "translate(" + (width / 2) + "," + (height / 2) + ")" | |
}); | |
isometric = function(_3d_p) { | |
return [-Math.sqrt(3) / 2 * _3d_p[0] + Math.sqrt(3) / 2 * _3d_p[1], +0.5 * _3d_p[0] + 0.5 * _3d_p[1] - _3d_p[2]]; | |
}; | |
parallelepipedon = function(d) { | |
var fb, ft, mlb, mlt, mrb, mrt, nb, nt; | |
if (!(d.x != null)) { | |
d.x = 0; | |
} | |
if (!(d.y != null)) { | |
d.y = 0; | |
} | |
if (!(d.z != null)) { | |
d.z = 0; | |
} | |
if (!(d.dx != null)) { | |
d.dx = 10; | |
} | |
if (!(d.dy != null)) { | |
d.dy = 10; | |
} | |
if (!(d.dz != null)) { | |
d.dz = 10; | |
} | |
fb = isometric([d.x, d.y, d.z], mlb = isometric([d.x + d.dx, d.y, d.z], nb = isometric([d.x + d.dx, d.y + d.dy, d.z], mrb = isometric([d.x, d.y + d.dy, d.z], ft = isometric([d.x, d.y, d.z + d.dz], mlt = isometric([d.x + d.dx, d.y, d.z + d.dz], nt = isometric([d.x + d.dx, d.y + d.dy, d.z + d.dz], mrt = isometric([d.x, d.y + d.dy, d.z + d.dz])))))))); | |
d.iso = { | |
face_bottom: [fb, mrb, nb, mlb], | |
face_left: [mlb, mlt, nt, nb], | |
face_right: [nt, mrt, mrb, nb], | |
face_top: [ft, mrt, nt, mlt], | |
outline: [ft, mrt, mrb, nb, mlb, mlt], | |
far_point: fb | |
}; | |
return d; | |
}; | |
iso_layout = function(data, shape, scale) { | |
if (!(scale != null)) { | |
scale = 1; | |
} | |
data.forEach(function(d) { | |
return shape(d, scale); | |
}); | |
return data.sort(function(a, b) { | |
return a.iso.far_point[1] - b.iso.far_point[1]; | |
}); | |
}; | |
path_generator = function(d) { | |
return 'M' + d.map(function(p) { | |
return p.join(' '); | |
}).join('L') + 'z'; | |
}; | |
y_color = d3.scale.category10(); | |
L = 30; | |
PAD = 6; | |
data = d3.range(6 * 6).map(function(i) { | |
return { | |
x: (i % 6) * L, | |
y: Math.floor(i / 6) * L, | |
dx: L - PAD, | |
dy: L - PAD, | |
dz: 10 + Math.random() * 6 * L | |
}; | |
}); | |
iso_layout(data, parallelepipedon); | |
pipedons = vis.selectAll('.pipedon').data(data); | |
enter_pipedons = pipedons.enter().append('g').attr({ | |
"class": 'pipedon' | |
}); | |
enter_pipedons.append('path').attr({ | |
"class": 'iso face bottom', | |
d: function(d) { | |
return path_generator(d.iso.face_bottom); | |
} | |
}); | |
enter_pipedons.append('path').attr({ | |
"class": 'iso face left template', | |
d: function(d) { | |
return path_generator(d.iso.face_left); | |
}, | |
fill: function(d) { | |
return y_color(d.y); | |
} | |
}); | |
enter_pipedons.append('path').attr({ | |
"class": 'iso face right', | |
d: function(d) { | |
return path_generator(d.iso.face_right); | |
}, | |
fill: function(d) { | |
var color; | |
color = d3.hcl(d3.select(this.parentNode).select('.template').style('fill')); | |
return d3.hcl(color.h, color.c, color.l - 12); | |
} | |
}); | |
enter_pipedons.append('path').attr({ | |
"class": 'iso face top', | |
d: function(d) { | |
return path_generator(d.iso.face_top); | |
}, | |
fill: function(d) { | |
var color; | |
color = d3.hcl(d3.select(this.parentNode).select('.template').style('fill')); | |
return d3.hcl(color.h, color.c, color.l + 12); | |
} | |
}); | |
enter_pipedons.append('path').attr({ | |
"class": 'iso outline', | |
d: function(d) { | |
return path_generator(d.iso.outline); | |
} | |
}); | |
}).call(this); |