Skip to content

Instantly share code, notes, and snippets.

@dgwyer
Created August 11, 2017 12:39
Show Gist options
  • Save dgwyer/bbd04bbcc3392001d8409f9eaa8ee776 to your computer and use it in GitHub Desktop.
Save dgwyer/bbd04bbcc3392001d8409f9eaa8ee776 to your computer and use it in GitHub Desktop.
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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment