Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active August 29, 2015 14:01
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save nitaku/6561455d304987f48ae5 to your computer and use it in GitHub Desktop.
OpeNER - Opinion targets (London)

This word cloud shows opinion target words from reviews of accommodations in London. Bigger words were identified more often by sentiment analyzers. Color represents the aggregated sentiment towards the target words (more green is more positive, more red is more negative and a yellow color is neutral). For example, both "hotel" and "room" have been mentioned a lot, but the overall sentiment towards "hotel" is more positive than the one towards "room".

Compared to a word cloud built with the same technique for the city of Amsterdam (see it here), this is more shallow and yellow, indicating a more neutral sentiment for London's opinion target words.

Use the mouse to pan and zoom.

Data is obtained from the Tour-Pedia APIs, that in turn get their input from the sentiment analysis pipeline of the OpeNER project.

The implementation makes use of d3.layout.cloud by Jason Davies.

html, body {
margin: 0;
padding: 0;
background: white;
}
text {
pointer-events:none;
}
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="OpeNER - Opinion targets (London)" />
<meta charset="utf-8">
<title>OpeNER - Opinion targets (London)</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src='layout.cloud.js'></script>
<script src="http://d3js.org/queue.v1.min.js"></script>
<script src="http://davidbau.com/encode/seedrandom-min.js"></script>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<script src='index.js'></script>
</body>
</html>
// DATA
var words = {"jumper":{"polarity":-1,"count":1},"tub":{"polarity":2,"count":2},"place":{"polarity":15,"count":31},"photo":{"polarity":1,"count":1},"staff":{"polarity":48,"count":60},"piccadilly":{"polarity":0,"count":2},"teens":{"polarity":-1,"count":1},"food":{"polarity":8,"count":20},"pool":{"polarity":5,"count":5},"service":{"polarity":13,"count":37},"chocolate":{"polarity":5,"count":5},"":{"polarity":40,"count":172},"reader":{"polarity":-2,"count":2},"elevator":{"polarity":1,"count":1},"stranger":{"polarity":1,"count":1},"cocktail":{"polarity":1,"count":1},"vibe":{"polarity":1,"count":1},"hotel":{"polarity":50,"count":76},"tv":{"polarity":-5,"count":7},"renovation":{"polarity":1,"count":1},"room":{"polarity":18,"count":82},"cleanliness":{"polarity":-1,"count":1},"girl":{"polarity":1,"count":1},"gym":{"polarity":0,"count":6},"wifi":{"polarity":29,"count":59},"policy":{"polarity":-1,"count":1},"fact":{"polarity":-1,"count":1},"crazy":{"polarity":-1,"count":1},"restaurant":{"polarity":3,"count":15},"bed":{"polarity":6,"count":12},"slab":{"polarity":1,"count":1},"woodman":{"polarity":1,"count":1},"football":{"polarity":1,"count":1},"egg":{"polarity":1,"count":1},"loyalty":{"polarity":1,"count":1},"point":{"polarity":2,"count":2},"night":{"polarity":-1,"count":11},"toilet":{"polarity":1,"count":1},"bar":{"polarity":6,"count":18},"tea":{"polarity":0,"count":2},"teapot":{"polarity":1,"count":1},"breakfast":{"polarity":11,"count":25},"hilton":{"polarity":2,"count":6},"location":{"polarity":42,"count":50},"front":{"polarity":0,"count":2},"city":{"polarity":4,"count":6},"icon":{"polarity":1,"count":1},"hhonors":{"polarity":1,"count":1},"pant":{"polarity":-1,"count":1},"buffet":{"polarity":1,"count":1},"kingsize":{"polarity":1,"count":1},"marks":{"polarity":1,"count":1},"desk":{"polarity":4,"count":4},"bit":{"polarity":2,"count":4},"sushi":{"polarity":1,"count":1},"view":{"polarity":25,"count":27},"sunday":{"polarity":0,"count":2},"lounge":{"polarity":4,"count":4},"trip":{"polarity":2,"count":2},"way":{"polarity":4,"count":4},"joke":{"polarity":-1,"count":1},"area":{"polarity":1,"count":5},"relative":{"polarity":1,"count":1},"opening":{"polarity":-1,"count":1},"birthday":{"polarity":1,"count":3},"protips":{"polarity":1,"count":1},"execution":{"polarity":-1,"count":1},"animal":{"polarity":1,"count":1},"box":{"polarity":2,"count":2},"touch":{"polarity":2,"count":2},"chessington":{"polarity":-1,"count":1},"cable":{"polarity":-1,"count":1},"hell":{"polarity":-1,"count":1},"page":{"polarity":-1,"count":1},"shower":{"polarity":6,"count":12},"business":{"polarity":3,"count":3},"speed":{"polarity":-2,"count":2},"option":{"polarity":1,"count":1},"leaf":{"polarity":1,"count":1},"afternoon":{"polarity":1,"count":1},"musician":{"polarity":1,"count":1},"pastry":{"polarity":3,"count":3},"traditional":{"polarity":1,"count":1},"coco":{"polarity":1,"count":1},"sofitel":{"polarity":1,"count":1},"sospa":{"polarity":1,"count":1},"salad":{"polarity":1,"count":1},"treaty":{"polarity":1,"count":1},"iphone":{"polarity":-1,"count":1},"woman":{"polarity":1,"count":1},"member":{"polarity":1,"count":1},"club":{"polarity":4,"count":6},"fence":{"polarity":-1,"count":1},"part":{"polarity":1,"count":1},"park":{"polarity":1,"count":1},"petersham":{"polarity":1,"count":1},"december":{"polarity":2,"count":2},"muffin":{"polarity":1,"count":1},"surprise":{"polarity":2,"count":2},"building":{"polarity":2,"count":2},"london":{"polarity":6,"count":6},"corner":{"polarity":-2,"count":2},"snack":{"polarity":4,"count":4},"rooftop":{"polarity":1,"count":1},"carpark":{"polarity":1,"count":1},"await":{"polarity":1,"count":1},"water":{"polarity":2,"count":6},"couple":{"polarity":-1,"count":1},"storeroom":{"polarity":-1,"count":1},"evening":{"polarity":1,"count":1},"atmosphere":{"polarity":0,"count":2},"response":{"polarity":1,"count":1},"occasion":{"polarity":1,"count":1},"opportunity":{"polarity":1,"count":1},"something":{"polarity":2,"count":4},"ice-cream":{"polarity":1,"count":1},"guest":{"polarity":0,"count":4},"feedback":{"polarity":1,"count":1},"tango":{"polarity":1,"count":1},"dancer":{"polarity":1,"count":1},"round":{"polarity":1,"count":1},"video":{"polarity":-1,"count":1},"four":{"polarity":1,"count":1},"friday":{"polarity":4,"count":4},"friend":{"polarity":-1,"count":1},"person":{"polarity":5,"count":13},"money":{"polarity":-1,"count":1},"http://www.whosjack.org/14hotspot/drink-shop-and-dance/..":{"polarity":1,"count":1},"list":{"polarity":1,"count":1},"morning":{"polarity":0,"count":2},"anything":{"polarity":1,"count":1},"pics":{"polarity":1,"count":1},"camdeners":{"polarity":1,"count":1},"bathing":{"polarity":1,"count":1},"social":{"polarity":1,"count":1},"bianca":{"polarity":1,"count":1},"customer":{"polarity":2,"count":2},"access":{"polarity":5,"count":5},"ticket":{"polarity":1,"count":1},"experience":{"polarity":3,"count":3},"jarek":{"polarity":-1,"count":1},"case":{"polarity":1,"count":1},"envoy":{"polarity":1,"count":1},"chance":{"polarity":-1,"count":1},"moment":{"polarity":1,"count":1},"word":{"polarity":-1,"count":1},"picture":{"polarity":1,"count":1},"look":{"polarity":2,"count":2},"’":{"polarity":1,"count":1},"coffee":{"polarity":5,"count":5},"tidy":{"polarity":1,"count":1},"chinese":{"polarity":1,"count":1},"02":{"polarity":1,"count":1},"pub":{"polarity":2,"count":2},"o2":{"polarity":1,"count":3},"/":{"polarity":1,"count":3},"mini":{"polarity":1,"count":1},"andaz":{"polarity":1,"count":1},"meeting":{"polarity":1,"count":3},"orderd":{"polarity":1,"count":1},"extra":{"polarity":2,"count":2},"massage":{"polarity":1,"count":1},"don":{"polarity":-1,"count":3},"event":{"polarity":1,"count":1},"sitting":{"polarity":1,"count":1},"ceiling":{"polarity":-2,"count":2},"v":{"polarity":-1,"count":1},"style":{"polarity":1,"count":1},"interior":{"polarity":1,"count":1},"ambience":{"polarity":2,"count":2},"couldn":{"polarity":1,"count":1},"price":{"polarity":16,"count":20},"king":{"polarity":0,"count":2},"temple":{"polarity":1,"count":1},"eastern":{"polarity":1,"count":1},"standard":{"polarity":1,"count":1},"thai":{"polarity":-1,"count":1},"sausage":{"polarity":-1,"count":1},"effort":{"polarity":1,"count":1},"cosy":{"polarity":1,"count":1},"timothy":{"polarity":1,"count":1},"cool":{"polarity":1,"count":1},"ukelele":{"polarity":1,"count":1},"bag":{"polarity":1,"count":1},"cereal":{"polarity":-1,"count":1},"bread":{"polarity":-1,"count":1},"hostel":{"polarity":2,"count":8},"bedroom":{"polarity":0,"count":2},"hair":{"polarity":0,"count":2},"kids.":{"polarity":-1,"count":1},"earth":{"polarity":-1,"count":1},"underground":{"polarity":-1,"count":1},"choice":{"polarity":-1,"count":3},"rate":{"polarity":2,"count":6},"executive":{"polarity":-1,"count":3},"today":{"polarity":1,"count":1},"st":{"polarity":-1,"count":1},"killer":{"polarity":-1,"count":1},"lunchtime":{"polarity":1,"count":1},"queen":{"polarity":1,"count":1},"belgravia":{"polarity":1,"count":1},"beauty":{"polarity":1,"count":1},"surroundings":{"polarity":1,"count":1},"heating":{"polarity":0,"count":2},"house":{"polarity":1,"count":1},"rude":{"polarity":-1,"count":1},"champagne":{"polarity":-1,"count":1},"tea-ing":{"polarity":1,"count":1},"lot":{"polarity":2,"count":2},"beer":{"polarity":3,"count":5},"dns":{"polarity":-2,"count":2},"everything":{"polarity":1,"count":1},"value":{"polarity":11,"count":11},"spa":{"polarity":1,"count":3},"doesn":{"polarity":2,"count":2},"section":{"polarity":1,"count":1},"headphone":{"polarity":-1,"count":1},"meal":{"polarity":2,"count":2},"dining":{"polarity":1,"count":3},"furniture":{"polarity":1,"count":1},"course":{"polarity":-1,"count":1},"second":{"polarity":1,"count":1},"sink":{"polarity":-1,"count":1},"quad":{"polarity":-2,"count":2},"amsterdam":{"polarity":1,"count":1},"card":{"polarity":-1,"count":1},"glass":{"polarity":1,"count":1},"wine":{"polarity":2,"count":2},"drink":{"polarity":3,"count":3},"airport":{"polarity":2,"count":2},"portion":{"polarity":-1,"count":1},"information":{"polarity":-1,"count":1},"being":{"polarity":1,"count":1},"bottle":{"polarity":1,"count":1},"camp":{"polarity":-1,"count":1},"plate":{"polarity":1,"count":1},"training":{"polarity":1,"count":1},"must-try":{"polarity":1,"count":1},"cuisine":{"polarity":1,"count":1},"£":{"polarity":-3,"count":3},"internet":{"polarity":4,"count":12},"iphones":{"polarity":-1,"count":1},"socket":{"polarity":-1,"count":1},"margarine":{"polarity":-1,"count":1},"great":{"polarity":2,"count":2},"summer":{"polarity":1,"count":1},"watch":{"polarity":1,"count":1},"traffic":{"polarity":1,"count":1},"":{"polarity":-1,"count":1},"eye":{"polarity":1,"count":1},"street":{"polarity":-1,"count":3},"abbey":{"polarity":1,"count":1},"car":{"polarity":-1,"count":3},"crossing":{"polarity":1,"count":1},"driver":{"polarity":0,"count":2},"landmark":{"polarity":1,"count":1},"sense":{"polarity":1,"count":1},"shite":{"polarity":-1,"count":1},"timing":{"polarity":2,"count":2},"album":{"polarity":1,"count":1},"floor":{"polarity":1,"count":5},"backpacker":{"polarity":1,"count":1},"amenity":{"polarity":1,"count":1},"wi-fi":{"polarity":5,"count":5},"side":{"polarity":-2,"count":2},"deluxe":{"polarity":1,"count":1},"internet.":{"polarity":1,"count":1},"center":{"polarity":1,"count":1},"trail":{"polarity":-1,"count":1},"ciapirina":{"polarity":1,"count":1},"concierge":{"polarity":3,"count":3},"sea":{"polarity":1,"count":1},"scone":{"polarity":1,"count":1},"tune":{"polarity":1,"count":1},"drawing":{"polarity":1,"count":1},"dish":{"polarity":1,"count":1},"hug":{"polarity":1,"count":1},"gm":{"polarity":1,"count":1},"lobby":{"polarity":2,"count":4},"nonexistent":{"polarity":-1,"count":1},"facility":{"polarity":2,"count":4},"shopping":{"polarity":1,"count":1},"greets":{"polarity":1,"count":1},"sound":{"polarity":0,"count":2},"deal":{"polarity":-1,"count":1},"central":{"polarity":-1,"count":1},"reception":{"polarity":-1,"count":5},"pipe":{"polarity":1,"count":1},"connectivity":{"polarity":1,"count":1},"times":{"polarity":1,"count":1},"coverage":{"polarity":0,"count":2},"size":{"polarity":1,"count":3},"connection":{"polarity":-1,"count":1},"room.":{"polarity":1,"count":1},"hour":{"polarity":4,"count":4},"amount":{"polarity":1,"count":1},"umami":{"polarity":1,"count":1},"outside":{"polarity":1,"count":1},"brownfox":{"polarity":1,"count":1},"tbh":{"polarity":-1,"count":1},"enough":{"polarity":1,"count":1},"need":{"polarity":-1,"count":1},"time":{"polarity":1,"count":5},"very":{"polarity":2,"count":2},"claen.":{"polarity":1,"count":1},"wise":{"polarity":1,"count":1},"camden":{"polarity":1,"count":1},"class":{"polarity":-1,"count":1},"brasserie":{"polarity":-1,"count":1},"english":{"polarity":1,"count":1},"thing":{"polarity":2,"count":2},"ask":{"polarity":-1,"count":1},"tube":{"polarity":1,"count":1},"movie":{"polarity":-2,"count":2},"week":{"polarity":0,"count":2},"walk":{"polarity":1,"count":1},"in-room":{"polarity":-1,"count":1},"cuz":{"polarity":1,"count":1},"starbucks":{"polarity":1,"count":1},"stay":{"polarity":4,"count":6},"life":{"polarity":1,"count":1},"sign":{"polarity":0,"count":2},"family":{"polarity":1,"count":3},"hyde":{"polarity":2,"count":2},"entrance":{"polarity":-2,"count":2},"washing":{"polarity":-1,"count":1},"load":{"polarity":0,"count":2},"child":{"polarity":2,"count":2},"shaped":{"polarity":-1,"count":1},"washer":{"polarity":-1,"count":1},"travel":{"polarity":-2,"count":2},"student":{"polarity":-1,"count":1},"key":{"polarity":-2,"count":2},"sleep":{"polarity":-1,"count":3},"bet":{"polarity":1,"count":1},"museum":{"polarity":1,"count":1},"tour":{"polarity":-1,"count":1},"parking":{"polarity":3,"count":5},"awake":{"polarity":1,"count":1},"nothing":{"polarity":0,"count":2},"home":{"polarity":1,"count":1},"minute":{"polarity":-1,"count":1},"lift":{"polarity":1,"count":1},"weird":{"polarity":-1,"count":1},"polo":{"polarity":3,"count":3},"hairdryer":{"polarity":1,"count":1},"bathroom":{"polarity":1,"count":3},"shirt":{"polarity":-1,"count":1},"housekeeping":{"polarity":1,"count":1},"hour.":{"polarity":-1,"count":1},"menu":{"polarity":1,"count":1},"brilliant":{"polarity":1,"count":1},"thought":{"polarity":1,"count":1},"tip":{"polarity":-1,"count":1},"worker":{"polarity":-1,"count":1},"dolcettis":{"polarity":2,"count":2},"antonella":{"polarity":1,"count":1},"charge":{"polarity":1,"count":1},"average":{"polarity":1,"count":1},"small":{"polarity":-1,"count":1},"swimming":{"polarity":-1,"count":1},"environment":{"polarity":2,"count":2},"penny":{"polarity":1,"count":1},"residence":{"polarity":-2,"count":2},"space":{"polarity":1,"count":3},"air":{"polarity":-1,"count":1},"hand":{"polarity":1,"count":1},"roof":{"polarity":1,"count":1},"saturday":{"polarity":1,"count":1},"singer":{"polarity":1,"count":1},"music":{"polarity":1,"count":1},"star":{"polarity":1,"count":1},"oxford":{"polarity":1,"count":1},"steak":{"polarity":2,"count":2},"prison":{"polarity":1,"count":1},"fitting":{"polarity":1,"count":1},"attitude":{"polarity":1,"count":1},"ucl":{"polarity":-1,"count":1},"head":{"polarity":1,"count":1},"wellness":{"polarity":1,"count":1},"sauna":{"polarity":1,"count":1},"bday":{"polarity":1,"count":1},"marble":{"polarity":1,"count":1},"nicety":{"polarity":-1,"count":1},"linen":{"polarity":1,"count":1},"window":{"polarity":1,"count":1},"excel":{"polarity":1,"count":1},"league":{"polarity":1,"count":1},"sun":{"polarity":1,"count":1},"heater":{"polarity":1,"count":1},"pity":{"polarity":-1,"count":1},"pillow":{"polarity":-1,"count":1},"play":{"polarity":1,"count":1},"bartender":{"polarity":-1,"count":1},"offer":{"polarity":1,"count":1},"call":{"polarity":1,"count":1},"dinner":{"polarity":1,"count":1},"minibar":{"polarity":-1,"count":1},"treatment":{"polarity":-1,"count":1},"adapter":{"polarity":-1,"count":1},"chargebox":{"polarity":1,"count":1},"man":{"polarity":1,"count":1},"serice":{"polarity":-1,"count":1},"number":{"polarity":-1,"count":1},"singapore":{"polarity":1,"count":1},"stomach":{"polarity":-1,"count":1},"boris":{"polarity":1,"count":1},"resto":{"polarity":1,"count":1},"spot":{"polarity":-1,"count":1},"dude":{"polarity":1,"count":1},"luggage":{"polarity":0,"count":2},"neighbour":{"polarity":-1,"count":1},"noisy":{"polarity":1,"count":1},"earplug":{"polarity":1,"count":1},"apartment":{"polarity":1,"count":1},"top":{"polarity":-1,"count":1},"secret":{"polarity":-1,"count":1},"yard":{"polarity":1,"count":1},"quality":{"polarity":1,"count":1},"living":{"polarity":1,"count":1},"one":{"polarity":-1,"count":1},"origin":{"polarity":1,"count":1},"barman":{"polarity":1,"count":1},"god":{"polarity":1,"count":1},"oil":{"polarity":-1,"count":1},"unit":{"polarity":-1,"count":1},"ring":{"polarity":0,"count":2},"receptionist":{"polarity":-1,"count":1},"college":{"polarity":-1,"count":1},"paddington":{"polarity":1,"count":1},"hall":{"polarity":-1,"count":1},"splurge":{"polarity":1,"count":1},"start":{"polarity":1,"count":1},"day":{"polarity":-1,"count":3},"skype":{"polarity":1,"count":1},"euston":{"polarity":0,"count":2},"fi":{"polarity":1,"count":1},"eh":{"polarity":-1,"count":1},"nice":{"polarity":-1,"count":1},"workout":{"polarity":0,"count":2},"memory":{"polarity":1,"count":1},"garden":{"polarity":2,"count":2},"party":{"polarity":0,"count":2},"detail":{"polarity":1,"count":1},"paintwork":{"polarity":-1,"count":1},"show":{"polarity":-1,"count":1},"disorder":{"polarity":-1,"count":1},"site":{"polarity":1,"count":1},"extortion":{"polarity":-1,"count":1},"orange":{"polarity":-1,"count":1},"%":{"polarity":-1,"count":1},"selection":{"polarity":1,"count":1},"condition":{"polarity":1,"count":1},"décor":{"polarity":-1,"count":1},"google":{"polarity":1,"count":1},"am":{"polarity":1,"count":1},"onion":{"polarity":1,"count":1},"shuttle":{"polarity":2,"count":2},"exec":{"polarity":1,"count":1},"wank":{"polarity":-1,"count":1},"crowd":{"polarity":-1,"count":1},"rock":{"polarity":-1,"count":1}};
Math.seedrandom('abcde'); // define a fixed random seed, to avoid to have a different layout on each page reload. change the string to randomize
var words_frequency = [];
Object.keys(words).forEach(function(word_lemma){
o = words[word_lemma];
o.lemma = word_lemma;
words_frequency.push(o);
});
var max = d3.max(Object.keys(words), function(k){
return words[k].count;
});
console.log(max);
var font_size = d3.scale.linear()
.domain([1, max])
.range([10,100]);
var color = d3.scale.linear()
.domain([-max, 0, max])
.range([d3.hcl(36, 65, 50), d3.hcl(95, 65, 80), d3.hcl(150, 65, 50)])
.interpolate(d3.interpolateHcl);
/*var color = d3.scale.quantize()
.domain([-max, max])
.range([d3.hcl(36, 65, 50), d3.hcl(150, 65, 50)]);*/
words_frequency.sort(function(a,b){
return b.count - a.count;
});
d3.layout.cloud().size([960, 500])
.words(words_frequency)
.rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.spiral('rectangular')
.text(function(d){ return d.lemma; })
.fontSize(function(d){ return font_size(d.count); })
.on("end", draw)
.start();
function draw(words) {
var svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500);
// append a group for zoomable content
var zoom_group = svg.append('g');
// define a zoom behavior
var zoom = d3.behavior.zoom()
.scaleExtent([1,4]) // min-max zoom
.on('zoom', function() {
// whenever the user zooms,
// modify translation and scale of the zoom group accordingly
zoom_group.attr('transform', 'translate('+zoom.translate()+')scale('+zoom.scale()+')');
});
// bind the zoom behavior to the main SVG
svg.call(zoom);
zoom_group.append("g")
.attr("transform", "translate(480,250)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d){ return font_size(d.count) + "px"; })
.style("font-family", "Impact")
.style("fill", function(d) { return color(d.polarity); })
.attr("text-anchor", "middle")
.attr("transform", function(d) {
var far = 1500*(Math.random() > 0.5 ? +1 : -1);
if(d.rotate === 0)
return "translate("+far+",0)rotate(" + d.rotate + ")";
else
return "translate(0,"+far+")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.lemma; })
.transition().duration(2000)
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
});
}
// d3.layout.cloud
// 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() {
function cloud() {
var size = [256, 256],
text = cloudText,
font = cloudFont,
fontSize = cloudFontSize,
fontStyle = cloudFontNormal,
fontWeight = cloudFontNormal,
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) {
d.text = text.call(this, d, i);
d.font = font.call(this, d, i);
d.style = fontStyle.call(this, d, i);
d.weight = fontWeight.call(this, d, i);
d.rotate = rotate.call(this, d, i);
d.size = ~~fontSize.call(this, d, i);
d.padding = padding.call(this, d, i);
return d;
}).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 (d.hasText && 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.fontStyle = function(x) {
if (!arguments.length) return fontStyle;
fontStyle = d3.functor(x);
return cloud;
};
cloud.fontWeight = function(x) {
if (!arguments.length) return fontWeight;
fontWeight = 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 cloudFontNormal() {
return "normal";
}
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.style + " " + d.weight + " " + ~~((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);
if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(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;
d.hasText = true;
x += w;
}
var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
sprite = [];
while (--di >= 0) {
d = data[di];
if (!d.hasText) continue;
var w = d.width,
w32 = w >> 5,
h = d.y1 - d.y0;
// 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;
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 {
// Attempt to use node-canvas.
canvas = new Canvas(cw << 5, ch);
}
var c = canvas.getContext("2d"),
spirals = {
archimedean: archimedeanSpiral,
rectangular: rectangularSpiral
};
c.fillStyle = c.strokeStyle = "red";
c.textAlign = "center";
if (typeof module === "object" && module.exports) module.exports = cloud;
else (d3.layout || (d3.layout = {})).cloud = cloud;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment