A mix of isometric and word cloud "treemaps".
forked from nitaku's block: Isometric word cloud
license: mit |
A mix of isometric and word cloud "treemaps".
forked from nitaku's block: 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); |