Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active August 29, 2015 14:08
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/f6f42f7f6b5dea054d18 to your computer and use it in GitHub Desktop.
Save nitaku/f6f42f7f6b5dea054d18 to your computer and use it in GitHub Desktop.
Opinion treemap word cloud (OpeNER - Paris, es)

This treemap word cloud shows opinion target words from reviews of accommodations in Paris. Bigger words were identified more often by sentiment analyzers. Color represents the aggregated sentiment towards the target words (green is positive, red is negative and yellow is neutral).

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.

# layout, behaviors and scales
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
treemap = d3.layout.treemap()
.size([width, height])
.value((node) -> node.count)
.sort((a,b) -> a.polarity-b.polarity)
.ratio(4)
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])
# translate the viewBox to have (0,0) at the center of the vis
svg
.attr
viewBox: "#{-width/2-5} #{-height/2-5} #{width+10} #{height+10}"
# 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(#{-width/2},#{-height/2})"
d3.json 'http://tour-pedia.org/api/getOpinions?location=Paris&language=es', (data) ->
words = data.target
words_frequency = []
best = 0
worst = 0
Object.keys(words).forEach (word_lemma) ->
o = words[word_lemma]
#return if o.count <= 1
o.lemma = word_lemma.trim()
return if o.lemma.length <= 1
words_frequency.push(o)
best = Math.max(best, o.polarity)
worst = Math.min(worst, o.polarity)
fake_tree = {
children: words_frequency
}
NEUTRAL = 80
color = d3.scale.linear()
.domain([worst, 0, best])
.range([d3.hcl(NEUTRAL-80, 85, 50), d3.hcl(NEUTRAL, 85, 70), d3.hcl(NEUTRAL+80, 85, 50)])
.interpolate(d3.interpolateHcl)
nodes_data = treemap.nodes(fake_tree)
labels = vis.selectAll('.label')
.data(nodes_data.filter((node) -> node.depth is 1))
enter_labels = labels.enter().append('svg')
.attr
class: 'label'
enter_labels.append('text')
.text((node) -> node.lemma.toUpperCase())
.attr
dy: '0.35em'
fill: (node) -> color(node.polarity)
.each (node) ->
bbox = this.getBBox()
#bbox_aspect = bbox.width / bbox.height
bbox_aspect = 2
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)')
enter_labels
.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}"
preserveAspectRatio: 'none'
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="Opinion treemap word cloud (OpeNER - Paris, es)" />
<title>Opinion treemap word cloud (OpeNER - Paris, es)</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 correct_x, correct_y, height, svg, treemap, vis, width, zoom, zoomable_layer;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
treemap = d3.layout.treemap().size([width, height]).value(function(node) {
return node.count;
}).sort(function(a, b) {
return a.polarity - b.polarity;
}).ratio(4);
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]);
svg.attr({
viewBox: "" + (-width / 2 - 5) + " " + (-height / 2 - 5) + " " + (width + 10) + " " + (height + 10)
});
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(" + (-width / 2) + "," + (-height / 2) + ")"
});
d3.json('http://tour-pedia.org/api/getOpinions?location=Paris&language=es', function(data) {
var NEUTRAL, best, color, enter_labels, fake_tree, labels, nodes_data, words, words_frequency, worst;
words = data.target;
words_frequency = [];
best = 0;
worst = 0;
Object.keys(words).forEach(function(word_lemma) {
var o;
o = words[word_lemma];
o.lemma = word_lemma.trim();
if (o.lemma.length <= 1) {
return;
}
words_frequency.push(o);
best = Math.max(best, o.polarity);
return worst = Math.min(worst, o.polarity);
});
fake_tree = {
children: words_frequency
};
NEUTRAL = 80;
color = d3.scale.linear().domain([worst, 0, best]).range([d3.hcl(NEUTRAL - 80, 85, 50), d3.hcl(NEUTRAL, 85, 70), d3.hcl(NEUTRAL + 80, 85, 50)]).interpolate(d3.interpolateHcl);
nodes_data = treemap.nodes(fake_tree);
labels = vis.selectAll('.label').data(nodes_data.filter(function(node) {
return node.depth === 1;
}));
enter_labels = labels.enter().append('svg').attr({
"class": 'label'
});
enter_labels.append('text').text(function(node) {
return node.lemma.toUpperCase();
}).attr({
dy: '0.35em',
fill: function(node) {
return color(node.polarity);
}
}).each(function(node) {
var bbox, bbox_aspect, node_bbox, node_bbox_aspect, rotate;
bbox = this.getBBox();
bbox_aspect = 2;
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)');
}
});
return enter_labels.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;
},
preserveAspectRatio: 'none'
});
});
}).call(this);
@nitaku
Copy link
Author

nitaku commented Oct 24, 2014

The color scale has some issues if the user is interested in comparing positive with negative polarity values. The most green word has not in general the same absolute value of the most red word, since positive and negative values are normalized separately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment