A mix of isometric and word cloud "treemaps".
Last active
August 16, 2017 21:04
-
-
Save nitaku/6210bd80bdd20181e1f4 to your computer and use it in GitHub Desktop.
Isometric word cloud
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
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) |
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
.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; | |
} |
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 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> |
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
// 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); |
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
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('') |
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
// 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