Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active August 29, 2015 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/10718152ec982ab3bca7 to your computer and use it in GitHub Desktop.
Save nitaku/10718152ec982ab3bca7 to your computer and use it in GitHub Desktop.
(Almost) stable word cloud
# layout, behaviors and scales
SCALE = 400
treemap = d3.layout.treemap()
.size([SCALE*1.3, SCALE])
.value((node) -> node.size)
correct_x = d3.scale.linear()
.domain([0, SCALE])
.range([0, 420])
correct_y = d3.scale.linear()
.domain([0, SCALE])
.range([0, 300])
color = (txt, light) ->
Math.seedrandom(txt+'abcdef')
noise = (W) -> Math.random()*W - W/2
d3.hcl(0+noise(360), 20, if light then 65 else 25)
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
# translate the viewBox to have (0,0) at the center of the vis
svg
.attr
viewBox: "#{-width/2} #{-height/2} #{width} #{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)
# group the visualization
vis = zoomable_layer.append('g')
.attr
transform: "translate(#{-SCALE*1.3/2},#{-SCALE/2})"
scores = [7, 6, 5, 4, 3, 2, 1]
redraw = (duration) ->
scores[Math.floor(Math.random()*7)] += Math.random()*3
tree = {
children: [
{name: 'alpha', size: scores[0]},
{name: 'beta', size: scores[1]},
{name: 'gamma', size: scores[2]},
{name: 'delta', size: scores[3]},
{name: 'epsilon', size: scores[4]},
{name: 'zeta', size: scores[5]},
{name: 'eta', size: scores[6]}
]
}
nodes_data = treemap.nodes(tree)
#nodes = vis.selectAll('.node')
# .data(nodes_data.filter((node) -> node.depth is 1))
#
#enter_nodes = nodes.enter().append('rect')
# .attr
# class: 'node'
# x: (node) -> node.x
# y: (node) -> node.y
# width: (node) -> node.dx
# height: (node) -> node.dy
# fill: (node) -> color(node.name, true)
labels = vis.selectAll('.label')
.data(nodes_data.filter((node) -> node.depth is 1))
enter_labels = labels.enter().append('svg')
.attr
class: 'label'
preserveAspectRatio: 'none'
enter_labels.append('text')
.text((node) -> node.name.toUpperCase())
.attr
dy: '0.35em'
fill: (node) -> color(node.name, false)
labels.select('text').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
node.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 node.rotate
node.label_bbox = {
x: node.label_bbox.y,
y: node.label_bbox.x,
width: node.label_bbox.height,
height: node.label_bbox.width
}
labels.transition().duration(duration)
.attr
x: (node) -> node.x
y: (node) -> node.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}"
.select('text')
.attr
transform: (node) -> if node.rotate then 'rotate(-90)' else 'rotate(0)'
setInterval((() -> redraw(1200)), 2000)
redraw(0)
svg {
background: white;
}
.node {
shape-rendering: crispEdges;
vector-effect: non-scaling-stroke;
stroke: white;
stroke-width: 2;
}
.label {
pointer-events: none;
text-anchor: middle;
font-family: Impact;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="(Almost) stable word cloud" />
<title>(Almost) stable word cloud</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://davidbau.com/encode/seedrandom-min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg height="500" width="960"></svg>
<script src="index.js"></script>
</body>
</html>
(function() {
var SCALE, color, correct_x, correct_y, height, redraw, scores, svg, treemap, vis, width, zoom, zoomable_layer;
SCALE = 400;
treemap = d3.layout.treemap().size([SCALE * 1.3, SCALE]).value(function(node) {
return node.size;
});
correct_x = d3.scale.linear().domain([0, SCALE]).range([0, 420]);
correct_y = d3.scale.linear().domain([0, SCALE]).range([0, 300]);
color = function(txt, light) {
var noise;
Math.seedrandom(txt + 'abcdef');
noise = function(W) {
return Math.random() * W - W / 2;
};
return d3.hcl(0 + noise(360), 20, light ? 65 : 25);
};
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
svg.attr({
viewBox: "" + (-width / 2) + " " + (-height / 2) + " " + width + " " + 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({
transform: "translate(" + (-SCALE*1.3 / 2) + "," + (-SCALE / 2) + ")"
});
scores = [7, 6, 5, 4, 3, 2, 1];
redraw = function(duration) {
var enter_labels, labels, nodes_data, tree;
scores[Math.floor(Math.random() * 7)] += Math.random() * 3;
tree = {
children: [
{
name: 'alpha',
size: scores[0]
}, {
name: 'beta',
size: scores[1]
}, {
name: 'gamma',
size: scores[2]
}, {
name: 'delta',
size: scores[3]
}, {
name: 'epsilon',
size: scores[4]
}, {
name: 'zeta',
size: scores[5]
}, {
name: 'eta',
size: scores[6]
}
]
};
nodes_data = treemap.nodes(tree);
labels = vis.selectAll('.label').data(nodes_data.filter(function(node) {
return node.depth === 1;
}));
enter_labels = labels.enter().append('svg').attr({
"class": 'label',
preserveAspectRatio: 'none'
});
enter_labels.append('text').text(function(node) {
return node.name.toUpperCase();
}).attr({
dy: '0.35em',
fill: function(node) {
return color(node.name, false);
}
});
labels.select('text').each(function(node) {
var bbox, bbox_aspect, node_bbox, node_bbox_aspect;
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;
node.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 (node.rotate) {
return node.label_bbox = {
x: node.label_bbox.y,
y: node.label_bbox.x,
width: node.label_bbox.height,
height: node.label_bbox.width
};
}
});
return labels.transition().duration(duration).attr({
x: function(node) {
return node.x;
},
y: function(node) {
return node.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;
}
}).select('text').attr({
transform: function(node) {
if (node.rotate) {
return 'rotate(-90)';
} else {
return 'rotate(0)';
}
}
});
};
setInterval((function() {
return redraw(1200);
}), 2000);
redraw(0);
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment