Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Topic Clouds using D3 Word Cloud Layout
// Word cloud layout by Jason Davies, http://www.jasondavies.com/word-cloud/
// Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf
(function(exports) {
function cloud() {
var size = [256, 256],
text = cloudText,
font = cloudFont,
fontSize = cloudFontSize,
rotate = cloudRotate,
padding = cloudPadding,
spiral = archimedeanSpiral,
words = [],
timeInterval = Infinity,
event = d3.dispatch("word", "end"),
timer = null,
cloud = {};
cloud.start = function() {
var board = zeroArray((size[0] >> 5) * size[1]),
bounds = null,
n = words.length,
i = -1,
tags = [],
data = words.map(function(d, i) {
return {
text: text.call(this, d, i),
font: font.call(this, d, i),
rotate: rotate.call(this, d, i),
size: ~~fontSize.call(this, d, i),
padding: cloudPadding.call(this, d, i)
};
}).sort(function(a, b) { return b.size - a.size; });
if (timer) clearInterval(timer);
timer = setInterval(step, 0);
step();
return cloud;
function step() {
var start = +new Date,
d;
while (+new Date - start < timeInterval && ++i < n && timer) {
d = data[i];
d.x = (size[0] * (Math.random() + .5)) >> 1;
d.y = (size[1] * (Math.random() + .5)) >> 1;
cloudSprite(d, data, i);
if (place(board, d, bounds)) {
tags.push(d);
event.word(d);
if (bounds) cloudBounds(bounds, d);
else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
// Temporary hack
d.x -= size[0] >> 1;
d.y -= size[1] >> 1;
}
}
if (i >= n) {
cloud.stop();
event.end(tags, bounds);
}
}
}
cloud.stop = function() {
if (timer) {
clearInterval(timer);
timer = null;
}
return cloud;
};
cloud.timeInterval = function(x) {
if (!arguments.length) return timeInterval;
timeInterval = x == null ? Infinity : x;
return cloud;
};
function place(board, tag, bounds) {
var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
startX = tag.x,
startY = tag.y,
maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
s = spiral(size),
dt = Math.random() < .5 ? 1 : -1,
t = -dt,
dxdy,
dx,
dy;
while (dxdy = s(t += dt)) {
dx = ~~dxdy[0];
dy = ~~dxdy[1];
if (Math.min(dx, dy) > maxDelta) break;
tag.x = startX + dx;
tag.y = startY + dy;
if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
// TODO only check for collisions within current bounds.
if (!bounds || !cloudCollide(tag, board, size[0])) {
if (!bounds || collideRects(tag, bounds)) {
var sprite = tag.sprite,
w = tag.width >> 5,
sw = size[0] >> 5,
lx = tag.x - (w << 4),
sx = lx & 0x7f,
msx = 32 - sx,
h = tag.y1 - tag.y0,
x = (tag.y + tag.y0) * sw + (lx >> 5),
last;
for (var j = 0; j < h; j++) {
last = 0;
for (var i = 0; i <= w; i++) {
board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
}
x += sw;
}
delete tag.sprite;
return true;
}
}
}
return false;
}
cloud.words = function(x) {
if (!arguments.length) return words;
words = x;
return cloud;
};
cloud.size = function(x) {
if (!arguments.length) return size;
size = [+x[0], +x[1]];
return cloud;
};
cloud.font = function(x) {
if (!arguments.length) return font;
font = d3.functor(x);
return cloud;
};
cloud.rotate = function(x) {
if (!arguments.length) return rotate;
rotate = d3.functor(x);
return cloud;
};
cloud.text = function(x) {
if (!arguments.length) return text;
text = d3.functor(x);
return cloud;
};
cloud.spiral = function(x) {
if (!arguments.length) return spiral;
spiral = spirals[x + ""] || x;
return cloud;
};
cloud.fontSize = function(x) {
if (!arguments.length) return fontSize;
fontSize = d3.functor(x);
return cloud;
};
cloud.padding = function(x) {
if (!arguments.length) return padding;
padding = d3.functor(x);
return cloud;
};
return d3.rebind(cloud, event, "on");
}
function cloudText(d) {
return d.text;
}
function cloudFont() {
return "serif";
}
function cloudFontSize(d) {
return Math.sqrt(d.value);
}
function cloudRotate() {
return (~~(Math.random() * 6) - 3) * 30;
}
function cloudPadding() {
return 1;
}
// Fetches a monochrome sprite bitmap for the specified text.
// Load in batches for speed.
function cloudSprite(d, data, di) {
if (d.sprite) return;
c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
var x = 0,
y = 0,
maxh = 0,
n = data.length;
di--;
while (++di < n) {
d = data[di];
c.save();
c.font = ~~((d.size + 1) / ratio) + "px " + d.font;
var w = c.measureText(d.text + "m").width * ratio,
h = d.size << 1;
if (d.rotate) {
var sr = Math.sin(d.rotate * cloudRadians),
cr = Math.cos(d.rotate * cloudRadians),
wcr = w * cr,
wsr = w * sr,
hcr = h * cr,
hsr = h * sr;
w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
} else {
w = (w + 0x1f) >> 5 << 5;
}
if (h > maxh) maxh = h;
if (x + w >= (cw << 5)) {
x = 0;
y += maxh;
maxh = 0;
}
if (y + h >= ch) break;
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
if (d.rotate) c.rotate(d.rotate * cloudRadians);
c.fillText(d.text, 0, 0);
c.restore();
d.width = w;
d.height = h;
d.xoff = x;
d.yoff = y;
d.x1 = w >> 1;
d.y1 = h >> 1;
d.x0 = -d.x1;
d.y0 = -d.y1;
x += w;
}
var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
sprite = [];
while (--di >= 0) {
d = data[di];
var w = d.width,
w32 = w >> 5,
h = d.y1 - d.y0,
p = d.padding;
// Zero the buffer
for (var i = 0; i < h * w32; i++) sprite[i] = 0;
x = d.xoff;
if (x == null) return;
y = d.yoff;
var seen = 0,
seenRow = -1;
for (var j = 0; j < h; j++) {
for (var i = 0; i < w; i++) {
var k = w32 * j + (i >> 5),
m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
if (p) {
if (j) sprite[k - w32] |= m;
if (j < w - 1) sprite[k + w32] |= m;
m |= (m << 1) | (m >> 1);
}
sprite[k] |= m;
seen |= m;
}
if (seen) seenRow = j;
else {
d.y0++;
h--;
j--;
y++;
}
}
d.y1 = d.y0 + seenRow;
d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
}
}
// Use mask-based collision detection.
function cloudCollide(tag, board, sw) {
sw >>= 5;
var sprite = tag.sprite,
w = tag.width >> 5,
lx = tag.x - (w << 4),
sx = lx & 0x7f,
msx = 32 - sx,
h = tag.y1 - tag.y0,
x = (tag.y + tag.y0) * sw + (lx >> 5),
last;
for (var j = 0; j < h; j++) {
last = 0;
for (var i = 0; i <= w; i++) {
if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
& board[x + i]) return true;
}
x += sw;
}
return false;
}
function cloudBounds(bounds, d) {
var b0 = bounds[0],
b1 = bounds[1];
if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
}
function collideRects(a, b) {
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
}
function archimedeanSpiral(size) {
var e = size[0] / size[1];
return function(t) {
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
};
}
function rectangularSpiral(size) {
var dy = 4,
dx = dy * size[0] / size[1],
x = 0,
y = 0;
return function(t) {
var sign = t < 0 ? -1 : 1;
// See triangular numbers: T_n = n * (n + 1) / 2.
switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
case 0: x += dx; break;
case 1: y += dy; break;
case 2: x -= dx; break;
default: y -= dy; break;
}
return [x, y];
};
}
// TODO reuse arrays?
function zeroArray(n) {
var a = [],
i = -1;
while (++i < n) a[i] = 0;
return a;
}
var cloudRadians = Math.PI / 180,
cw = 1 << 11 >> 5,
ch = 1 << 11,
canvas,
ratio = 1;
if (typeof document !== "undefined") {
canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
canvas.width = (cw << 5) / ratio;
canvas.height = ch / ratio;
} else {
// node-canvas support
var Canvas = require("canvas");
canvas = new Canvas(cw << 5, ch);
}
var c = canvas.getContext("2d"),
spirals = {
archimedean: archimedeanSpiral,
rectangular: rectangularSpiral
};
c.fillStyle = "red";
c.textAlign = "center";
exports.cloud = cloud;
})(typeof exports === "undefined" ? d3.layout || (d3.layout = {}) : exports);
<html>
<head>
<title>Topic Clouds</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<script src="http://d3js.org/d3.v2.min.js?2.10.0"></script>
<script type="text/javascript" src="d3.layout.cloud.js"></script>
</head>
<body>
<script>
wordScale=d3.scale.linear().domain([1,100,1000,10000]).range([10,20,40,80]).clamp(true);
wordColor=d3.scale.linear().domain([10,20,40,80]).range(["blue","green","orange","red"]);
for (x = 0; x < 3; x++) {
viz = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 440)
.attr("id", "svg" + x);
}
for (x = 0; x < 3; x++) {
//d3.json("topic.json", function(topic) {
d3.csv("topic_words"+x+".csv", function(topic) {
d3.layout.cloud().size([400, 400])
// .words([{"text":"test","size":wordScale(.01)},{"text":"bad","size":wordScale(1)}])
.words(topic)
.rotate(function() { return ~~(Math.random() * 2) * 5; })
.fontSize(function(d) { return wordScale(d.size); })
.on("end", draw)
.start();
function draw(words) {
viz = d3.select("#svg" + topic[0].topic);
viz.append("g")
.attr("transform", "translate(200,220)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d) { return d.size + "px"; })
.style("fill", function(d) { return wordColor(d.size); })
.style("opacity", .75)
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.text; });
viz
.append("text")
.data([topic[0]])
.style("font-size", 20)
.style("font-weight", 900)
.attr("x", 100)
.attr("y", 20)
.text(function(d) { return "TOPIC " + d.topic; })
// d3.select("#svg"+x).append("svg:text").text("Topic " + x);
// viz.enter().append("svg:text").text("Topic " + x);
}
})
}
</script>
text size topic access 1238 0 streets 1020 0 transportation 982 0 system 824 0 pedestrian 767 0 provide 763 0 bicycle 719 0 major 696 0 arterial 596 0 local 452 0 centers 415 0 service 388 0 connections 373 0 adjacent 337 0 arterials 335 0 facilities 317 0 modes 312 0 design 310 0 high 305 0 travel 284 0 areas 282 0 regional 275 0 serve 269 0 circulation 257 0 safe 241 0 vehicle 233 0 demand 228 0 roadway 223 0 improvements 218 0 corridors 211 0 capacity 208 0 activity 208 0 roadways 207 0 collector 202 0 modal 198 0 automobile 185 0 oriented 183 0 function 182 0 locations 182 0 light 174 0 convenient 172 0 trails 169 0 employment 156 0 classification 151 0 paths 151 0 designed 148 0 accommodate 136 0 primary 135 0 direct 135 0 type 131 0 wide 130 0 bicyclists 129 0 map 129 0 destinations 129 0 intersections 129 0 users 128 0 bikeways 126 0 complete 126 0 trail 120 0 bus 117 0 long 114 0 effective 113 0 providing 111 0 volumes 111 0 includes 110 0 standards 99 0 points 99 0 ride 99 0 truck 97 0 developments 97 0 emergency 94 0 bicycling 92 0 collectors 90 0 develop 86 0 motor 85 0 hours 80 0 additional 80 0 boulevard 79 0 balance 78 0 ridership 74 0 address 74 0 coordinate 72 0 serving 72 0 significant 72 0 stops 71 0 citywide 70 0 recreational 70 0 multiple 70 0 freeway 68 0 separated 68 0 minor 68 0 principal 68 0 highways 68 0 connect 66 0 amenities 66 0 landscaping 65 0 low 65 0 crossings 64 0 stop 64 0 sidewalks 63 0
text size topic buildings 460 1 plan 451 1 policy 442 1 neighborhoods 327 1 civic 301 1 community 249 1 strategies 235 1 existing 222 1 lots 221 1 walkable 217 1 large 207 1 arroyos 203 1 concerns 196 1 map 183 1 goal 182 1 spaces 182 1 neighborhood 182 1 streets 180 1 network 137 1 regional 136 1 addressing 131 1 compact 123 1 historic 110 1 element 105 1 valley 105 1 center 103 1 central 102 1 infill 101 1 small 95 1 space 92 1 greens 91 1 step 91 1 principles 91 1 design 89 1 blocks 88 1 drainage 87 1 drive 86 1 ave 82 1 smartcode 81 1 complete 81 1 impact 80 1 goals 80 1 legend 80 1 critical 79 1 plans 79 1 vacant 75 1 cities 74 1 current 74 1 green 72 1 sector 69 1 provided 68 1 arroyo 67 1 create 67 1 walk 67 1 proposed 65 1 fees 64 1 housing 64 1 preferred 64 1 northeast 64 1 war 63 1 eastside 63 1 officials 63 1 alleys 63 1 lined 62 1 world 61 1 mixed 59 1 created 59 1 smart 58 1 time 57 1 site 57 1 important 57 1 westside 57 1 surrounding 56 1 shed 56 1 lands 55 1 auto 54 1 homes 54 1 farmland 51 1 schools 51 1 subdivision 50 1 multiway 50 1 valuable 49 1 face 49 1 mission 49 1 owned 48 1 add 47 1 urbanism 47 1 shade 47 1 form 46 1 fabric 46 1 upper 46 1 added 46 1 sense 46 1 irrigated 45 1 title 45 1 regulations 45 1 edges 44 1 connected 43 1 fields 43 1 long 43 1
text size topic development 818 2 transit 746 2 centers 647 2 mixed 640 2 corridors 520 2 urban 443 2 city 414 2 oriented 388 2 building 359 2 areas 314 2 zoning 299 2 redevelopment 209 2 activity 198 2 blocks 195 2 districts 194 2 existing 190 2 vision 187 2 block 178 2 compact 166 2 investment 165 2 friendly 161 2 initiative 160 2 housing 158 2 standards 158 2 walking 154 2 develop 152 2 places 129 2 policies 127 2 people 125 2 stations 122 2 infill 117 2 options 114 2 sensitive 110 2 modal 108 2 walkable 106 2 design 105 2 quality 103 2 growth 103 2 creating 102 2 complete 101 2 key 99 2 private 91 2 homes 91 2 create 90 2 tod 88 2 encouraging 86 2 promotes 86 2 identify 83 2 concept 83 2 developments 83 2 mix 82 2 hubs 82 2 connected 81 2 direct 77 2 higher 77 2 vibrant 76 2 illustration 76 2 choices 75 2 built 74 2 includes 74 2 initiatives 73 2 core 68 2 corridor 67 2 scenario 65 2 jobs 64 2 shop 64 2 successful 64 2 type 62 2 principles 61 2 distance 59 2 practices 56 2 developer 53 2 destinations 53 2 exist 52 2 accommodate 51 2 sustainable 51 2 environment 51 2 central 50 2 patterns 49 2 developers 48 2 multifamily 48 2 future 47 2 pedestrian 47 2 small 46 2 investments 46 2 served 44 2 station 44 2 dense 42 2 trails 40 2 definition 40 2 similar 39 2 targeted 39 2 river 38 2 environments 38 2 natural 38 2 concentrations 36 2 walk 35 2 easy 35 2 university 35 2 boulevards 33 2
@codersofthedark

This comment has been minimized.

Copy link

codersofthedark commented Jul 23, 2013

  1. It should be topic_words2.csv and not topic_words3.csv
  2. I am getting following error on running the code:
    Uncaught TypeError: Cannot read property 'topic' of undefined cloud.html:36
    draw cloud.html:36
    t d3.v2.min.js:1
    step d3.layout.cloud.js:60
    cloud.start d3.layout.cloud.js:36
    (anonymous function) cloud.html:32
    (anonymous function) d3.v2.min.js:1
    r d3.v2.min.js:2
    r.onreadystatechange d3.v2.min.js:2
@adhoch

This comment has been minimized.

Copy link

adhoch commented Apr 9, 2014

The problem is that the csv's are all on one line. If you make each row a new line it will work fine.

@g1eb

This comment has been minimized.

Copy link

g1eb commented Nov 22, 2016

That link to Jason Davies paper on word cloud layout has moved to https://www.jasondavies.com/wordcloud/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.