Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active August 16, 2017 21:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/6210bd80bdd20181e1f4 to your computer and use it in GitHub Desktop.
Save nitaku/6210bd80bdd20181e1f4 to your computer and use it in GitHub Desktop.
Isometric word cloud
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
# append a group for zoomable content
zoomable_layer = svg.append('g')
# define a zoom behavior
zoom = d3.behavior.zoom()
.scaleExtent([1,10]) # min-max zoom
.on 'zoom', () ->
# GEOMETRIC ZOOM
zoomable_layer
.attr
transform: "translate(#{zoom.translate()})scale(#{zoom.scale()})"
# bind the zoom behavior to the main SVG
svg.call(zoom)
vis = zoomable_layer.append('g')
.attr
class: 'vis'
transform: "translate(#{width/2},#{height/3})"
# [x, y, h] -> [-Math.sqrt(3)/2*x+Math.sqrt(3)/2*y, 0.5*x+0.5*y-h]
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.h = 0 if not d.h?
d.dx = 10 if not d.dx?
d.dy = 10 if not d.dy?
d.dh = 10 if not d.dh?
fb = isometric [d.x, d.y, d.h],
mlb = isometric [d.x+d.dx, d.y, d.h],
nb = isometric [d.x+d.dx, d.y+d.dy, d.h],
mrb = isometric [d.x, d.y+d.dy, d.h],
ft = isometric [d.x, d.y, d.h+d.dh],
mlt = isometric [d.x+d.dx, d.y, d.h+d.dh],
nt = isometric [d.x+d.dx, d.y+d.dy, d.h+d.dh],
mrt = isometric [d.x, d.y+d.dy, d.h+d.dh]
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],
fb: fb,
mlb: mlb,
nb: nb,
mrb: mrb,
ft: ft,
mlt: mlt,
nt: nt,
mrt: mrt
}
return d
iso_layout = (data, shape, scale) ->
scale = 1 if not scale?
data.forEach (d) ->
shape(d, scale)
# this uses the treemap ordering in some way... (!!!)
data.sort (a,b) -> b.dh - a.dh
path_generator = (d) -> 'M' + d.map((p)->p.join(' ')).join('L') + 'z'
treemap = d3.layout.treemap()
.size([300, 300])
.value((d) -> d.area)
.sort((a,b) -> a.dh-b.dh)
.ratio(4)
.round(false) # bugfix: d3 wrong ordering
color = d3.scale.category20c()
correct_x = d3.scale.linear()
.domain([0, width])
.range([0, width*1.05])
correct_y = d3.scale.linear()
.domain([0, height])
.range([0, height*3/4])
data = d3.range(30).map () -> {word: randstring.new(), area: Math.random(), dh: Math.random()*150}
data = treemap.nodes({children: data}).filter (n) -> n.depth is 1
iso_layout(data, parallelepipedon)
data.forEach (d, i) ->
# save the template color
d.template_color = d3.hcl(color(i))
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'
d: (d) -> path_generator(d.iso.face_left)
fill: (d) -> d.template_color
enter_pipedons.append('path')
.attr
class: 'iso face right'
d: (d) -> path_generator(d.iso.face_right)
fill: (d) -> d3.hcl(d.template_color.h, d.template_color.c, d.template_color.l-12)
enter_pipedons.append('path')
.attr
class: 'iso face top'
d: (d) -> path_generator(d.iso.face_top)
fill: (d) -> d3.hcl(d.template_color.h, d.template_color.c, d.template_color.l+12)
enter_labels_g = enter_pipedons.append('g')
enter_labels = enter_labels_g.append('svg')
.attr
class: 'label'
enter_labels.append('text')
.text((d) -> d.word.toUpperCase())
.attr
dy: '.35em'
.each (node) ->
bbox = this.getBBox()
bbox_aspect = bbox.width / bbox.height
node_bbox = {width: node.dx, height: node.dy}
node_bbox_aspect = node_bbox.width / node_bbox.height
rotate = bbox_aspect >= 1 and node_bbox_aspect < 1 or bbox_aspect < 1 and node_bbox_aspect >= 1
node.label_bbox = {
x: bbox.x+(bbox.width-correct_x(bbox.width))/2,
y: bbox.y+(bbox.height-correct_y(bbox.height))/2,
width: correct_x(bbox.width),
height: correct_y(bbox.height)
}
if rotate
node.label_bbox = {
x: node.label_bbox.y,
y: node.label_bbox.x,
width: node.label_bbox.height,
height: node.label_bbox.width
}
d3.select(this).attr('transform', 'rotate(90) translate(0,1)')
enter_labels
.each (d) ->
d.iso_x = isometric([d.x+d.dx/2, d.y+d.dy/2, d.h+d.dh])[0]-d.dx/2
d.iso_y = isometric([d.x+d.dx/2, d.y+d.dy/2, d.h+d.dh])[1]-d.dy/2
enter_labels
.attr
x: (d) -> d.iso_x
y: (d) -> d.iso_y
width: (node) -> node.dx
height: (node) -> node.dy
viewBox: (node) -> "#{node.label_bbox.x} #{node.label_bbox.y} #{node.label_bbox.width} #{node.label_bbox.height}"
preserveAspectRatio: 'none'
fill: (d) -> d3.hcl(d.template_color.h, d.template_color.c, d.template_color.l-12)
enter_labels_g
.attr
transform: (d) -> "translate(#{d.iso_x+d.dx/2},#{d.iso_y+d.dy/2}) scale(1, #{1/Math.sqrt(3)}) rotate(-45) translate(#{-(d.iso_x+d.dx/2)},#{-(d.iso_y+d.dy/2)})"
enter_pipedons.append('path')
.attr
class: 'iso outline'
d: (d) -> path_generator(d.iso.outline)
.iso.face.bottom {
fill: brown;
}
.iso.outline {
stroke: #333;
fill: none;
vector-effect: non-scaling-stroke;
}
.vis:hover .pipedon:not(:hover) * {
opacity: 0.3;
}
.label {
pointer-events: none;
text-anchor: middle;
font-family: Impact;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Isometric Word Cloud</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="randstring.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 color, correct_x, correct_y, data, enter_labels, enter_labels_g, enter_pipedons, height, iso_layout, isometric, parallelepipedon, path_generator, pipedons, svg, treemap, vis, width, zoom, zoomable_layer;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
zoomable_layer = svg.append('g');
zoom = d3.behavior.zoom().scaleExtent([1, 10]).on('zoom', function() {
return zoomable_layer.attr({
transform: "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")"
});
});
svg.call(zoom);
vis = zoomable_layer.append('g').attr({
"class": 'vis',
transform: "translate(" + (width / 2) + "," + (height / 3) + ")"
});
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.h != null)) {
d.h = 0;
}
if (!(d.dx != null)) {
d.dx = 10;
}
if (!(d.dy != null)) {
d.dy = 10;
}
if (!(d.dh != null)) {
d.dh = 10;
}
fb = isometric([d.x, d.y, d.h], mlb = isometric([d.x + d.dx, d.y, d.h], nb = isometric([d.x + d.dx, d.y + d.dy, d.h], mrb = isometric([d.x, d.y + d.dy, d.h], ft = isometric([d.x, d.y, d.h + d.dh], mlt = isometric([d.x + d.dx, d.y, d.h + d.dh], nt = isometric([d.x + d.dx, d.y + d.dy, d.h + d.dh], mrt = isometric([d.x, d.y + d.dy, d.h + d.dh]))))))));
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],
fb: fb,
mlb: mlb,
nb: nb,
mrb: mrb,
ft: ft,
mlt: mlt,
nt: nt,
mrt: mrt
};
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 b.dh - a.dh;
});
};
path_generator = function(d) {
return 'M' + d.map(function(p) {
return p.join(' ');
}).join('L') + 'z';
};
treemap = d3.layout.treemap().size([300, 300]).value(function(d) {
return d.area;
}).sort(function(a, b) {
return a.dh - b.dh;
}).ratio(4).round(false);
color = d3.scale.category20c();
correct_x = d3.scale.linear().domain([0, width]).range([0, width * 1.05]);
correct_y = d3.scale.linear().domain([0, height]).range([0, height * 3 / 4]);
data = d3.range(30).map(function() {
return {
word: randstring["new"](),
area: Math.random(),
dh: Math.random() * 150
};
});
data = treemap.nodes({
children: data
}).filter(function(n) {
return n.depth === 1;
});
iso_layout(data, parallelepipedon);
data.forEach(function(d, i) {
return d.template_color = d3.hcl(color(i));
});
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',
d: function(d) {
return path_generator(d.iso.face_left);
},
fill: function(d) {
return d.template_color;
}
});
enter_pipedons.append('path').attr({
"class": 'iso face right',
d: function(d) {
return path_generator(d.iso.face_right);
},
fill: function(d) {
return d3.hcl(d.template_color.h, d.template_color.c, d.template_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) {
return d3.hcl(d.template_color.h, d.template_color.c, d.template_color.l + 12);
}
});
enter_labels_g = enter_pipedons.append('g');
enter_labels = enter_labels_g.append('svg').attr({
"class": 'label'
});
enter_labels.append('text').text(function(d) {
return d.word.toUpperCase();
}).attr({
dy: '.35em'
}).each(function(node) {
var bbox, bbox_aspect, node_bbox, node_bbox_aspect, rotate;
bbox = this.getBBox();
bbox_aspect = bbox.width / bbox.height;
node_bbox = {
width: node.dx,
height: node.dy
};
node_bbox_aspect = node_bbox.width / node_bbox.height;
rotate = bbox_aspect >= 1 && node_bbox_aspect < 1 || bbox_aspect < 1 && node_bbox_aspect >= 1;
node.label_bbox = {
x: bbox.x + (bbox.width - correct_x(bbox.width)) / 2,
y: bbox.y + (bbox.height - correct_y(bbox.height)) / 2,
width: correct_x(bbox.width),
height: correct_y(bbox.height)
};
if (rotate) {
node.label_bbox = {
x: node.label_bbox.y,
y: node.label_bbox.x,
width: node.label_bbox.height,
height: node.label_bbox.width
};
return d3.select(this).attr('transform', 'rotate(90) translate(0,1)');
}
});
enter_labels.each(function(d) {
d.iso_x = isometric([d.x + d.dx / 2, d.y + d.dy / 2, d.h + d.dh])[0] - d.dx / 2;
return d.iso_y = isometric([d.x + d.dx / 2, d.y + d.dy / 2, d.h + d.dh])[1] - d.dy / 2;
});
enter_labels.attr({
x: function(d) {
return d.iso_x;
},
y: function(d) {
return d.iso_y;
},
width: function(node) {
return node.dx;
},
height: function(node) {
return node.dy;
},
viewBox: function(node) {
return "" + node.label_bbox.x + " " + node.label_bbox.y + " " + node.label_bbox.width + " " + node.label_bbox.height;
},
preserveAspectRatio: 'none',
fill: function(d) {
return d3.hcl(d.template_color.h, d.template_color.c, d.template_color.l - 12);
}
});
enter_labels_g.attr({
transform: function(d) {
return "translate(" + (d.iso_x + d.dx / 2) + "," + (d.iso_y + d.dy / 2) + ") scale(1, " + (1 / Math.sqrt(3)) + ") rotate(-45) translate(" + (-(d.iso_x + d.dx / 2)) + "," + (-(d.iso_y + d.dy / 2)) + ")";
}
});
enter_pipedons.append('path').attr({
"class": 'iso outline',
d: function(d) {
return path_generator(d.iso.outline);
}
});
}).call(this);
window.randstring = {}
syllables = ['bi','bo','bu','ta','se','tri','su','ke','ka','flo','ko','pi','pe','no','go','zo','fu','fo','si','pa','ar','es','i','kya','kyu','fle','o','ne','na','le','lu','ma','an']
randlen = () -> 3+Math.floor(Math.random()*2)
randsy = () -> syllables[Math.floor(Math.random()*syllables.length)]
randstring.new = () -> (randsy() for j in [0...randlen()]).join('')
// Generated by CoffeeScript 1.4.0
(function() {
var randlen, randsy, syllables;
window.randstring = {};
syllables = ['bi', 'bo', 'bu', 'ta', 'se', 'tri', 'su', 'ke', 'ka', 'flo', 'ko', 'pi', 'pe', 'no', 'go', 'zo', 'fu', 'fo', 'si', 'pa', 'ar', 'es', 'i', 'kya', 'kyu', 'fle', 'o', 'ne', 'na', 'le', 'lu', 'ma', 'an'];
randlen = function() {
return 3 + Math.floor(Math.random() * 2);
};
randsy = function() {
return syllables[Math.floor(Math.random() * syllables.length)];
};
randstring["new"] = function() {
var j;
return ((function() {
var _i, _ref, _results;
_results = [];
for (j = _i = 0, _ref = randlen(); 0 <= _ref ? _i < _ref : _i > _ref; j = 0 <= _ref ? ++_i : --_i) {
_results.push(randsy());
}
return _results;
})()).join('');
};
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment