Skip to content

Instantly share code, notes, and snippets.

@dannycochran
Created June 1, 2013 03:49
Show Gist options
  • Save dannycochran/5689222 to your computer and use it in GitHub Desktop.
Save dannycochran/5689222 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>A River of Twitter Data</title>
<script src="http://d3js.org/d3.v2.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script type="text/javascript" src="http://f.cl.ly/items/2U0A2k0d1O2e0F2Y143F/jquery-ui.js"></script>
<script type="text/javascript" src="./tipsy.js"></script>
<link rel="stylesheet" href="./style.css" type="text/css" />
<link rel="stylesheet" href="./tipsy.css" type="text/css" />
<link rel="stylesheet" href="http://code.jquery.com/ui/1.9.1/themes/base/jquery-ui.css" />
</head>
<body>
<div>
<h1>
<img src="http://f.cl.ly/items/3S462I0S3v1M2F3d1L2V/twitter-bird-white-on-blue1.png"/>
Activity, Hurricane Sandy
<!-- play & pause buttons, hosted externally because Stanford doesn't seem to like these files on cgi-bin -->
<span><img class='playbutton' type="button" value="Play" id="play" src="http://f.cl.ly/items/3O2M2V2w2a0j0s1I2I32/Play.png"/></span>
<span><img class='playbutton' type="button" value="Stop" id="stop" style="display:none" src="http://f.cl.ly/items/3v452T2g0i0i1V1C113O/pause.png"/></span>
</h1>
</div>
<script type="text/javascript">
// distance bins
var distanceBins = [["0-50 Miles","Southern New Jersey"],["51-110 Miles","Includes Manhattan"],["111-200 Miles","Includes Washington D.C."],["201-500 Miles","Entire East Coast"],["501-1000 Miles","Midwest Region (including Chicago)"],["1001-3000 Miles","Continental U.S."],["≥ 3000 Miles","Rest of the World"]];
// for universal access, switchData contains the whole json file
var jsonData;
var jsonDataRT;
var jsonDataFilter;
var jsonDataRTFilter;
var switchData;
var checkData = 0;
// these control the user's potential selection range for brushing, too large a difference yields a scrunched graph
var max_extent_width = 10
, min_extent_width = 5;
// min/max values of d.positivity, d.freqBin, and d.objectivity
var positivityDomain = [0.0163966168908, 0.0418471720818]
, frequencyDomain = [1,4]
, objectivityDomain = [0.923212665835, 0.965169775079];
// vars for decreasing blueness (by increasing RG values) of heat maps
var redOffset = 0;
var greenOffset = 0;
d3.json("./sandyData.json", function (json) {
jsonData = json;
jsonDataRT = json;
switchData = json;
jsonDataFilter = switchData.filter(function(d) {return d.isRT == 0});
jsonDataRTFilter = switchData.filter(function(d) {return d.isRT == 1});
switchData = jsonDataFilter;
var margin = {top: 20, right: 15, bottom: 15, left: 105}
, width = 960 - margin.left - margin.right
, height = 500 - margin.top - margin.bottom
, miniHeight = distanceBins.length * 12 + 50
, mainHeight = height - miniHeight - 50;
x = d3.scale.linear()
.domain([0,164])
.range([1, width]);
x1 = d3.scale.linear().range([1, width]);
var ext = d3.extent(distanceBins, function(d,i) { return i; });
y1 = d3.scale.linear().domain([ext[0], ext[1] + 1]).range([0, mainHeight]);
y2 = d3.scale.linear().domain([ext[0], ext[1] + 1]).range([0, miniHeight]);
//heat maps for main and mini graph (sets blue (RG) B value)
colorScale = d3.scale.quantize()
.domain(positivityDomain)
.range([0,75,150,225]); // color will be arranged into four quartiles with quantize
// font size scale from 12 to 20
wordSizeScale = d3.scale.quantize()
.domain(frequencyDomain)
.range([12,14,16,18]);
// resizes fonts if user expands brush selection
extentSizeScale = d3.scale.linear()
.domain([min_extent_width,max_extent_width])
.range([2,4]);
// determines if bins are emotional enough to be italic
typeFaceScale = d3.scale.linear()
.domain(objectivityDomain)
.range([0,3]);
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left+105)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart');
chart.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', width)
.attr('height', mainHeight);
main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', mainHeight)
.attr('class', 'main');
mini = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + (mainHeight + 60) + ')')
.attr('width', width)
.attr('height', miniHeight)
.attr('class', 'mini');
// draw the distanceBins for the main chart
main.append('g').selectAll('.laneLines')
.data(distanceBins)
.enter().append('line')
.attr('x1', 0)
.attr('y1', function(d,i) { return d3.round(y1(i)) + 1; })
.attr('x2', width)
.attr('y2', function(d,i) { return d3.round(y1(i)) + 1; })
.attr('stroke', function(d,i) { return 'lightgray' });
main.append('g').selectAll('.laneText')
.data(distanceBins)
.enter().append('text')
.text(function(d) { return d[0]; })
.attr('x',-5)
.attr('y', function(d,i) { return y1(i + .5); })
.attr('dy', '0.5ex')
.attr('text-anchor', 'end')
.attr('class', 'laneText')
.attr('id', 'laneHover')
.style('font-family', 'Rockwell')
.on('mouseover',function() {
$('svg #laneHover').tipsy({
gravity: 'nw',
html: true,
title: function() {
var d = this.__data__;
return d[1];
}
})
});
// x-axis label
main.append('text')
.attr('class', 'x_label')
.attr('id', 'x_label')
.attr('text-anchor', 'end')
.attr('x', width+70)
.attr('y', mainHeight+5)
.style('font-family', 'Rockwell')
.text('hours from');
main.append('text')
.attr('class', 'x_label')
.attr('id', 'x_label')
.attr('text-anchor', 'end')
.attr('x', width+70)
.attr('y', mainHeight + 19)
.style('font-family', 'Rockwell')
.text('landfall');
// sloppy legend - how does one wrap SVG text?
main.append('svg:text')
.attr('x', width+7)
.attr('y', 15)
.style('font-family', 'Rockwell')
.style('font-size',12)
.text('Lighter shades of');
main.append('svg:text')
.attr('x', width+7)
.attr('y', 30)
.style('font-family', 'Rockwell')
.style('font-size',12)
.text('blue correspond to');
main.append('svg:text')
.attr('x', width+7)
.attr('y', 45)
.style('font-family', 'Rockwell')
.style('font-size',12)
.text('higher positivity.');
main.append('svg:text')
.attr('x', width+7)
.attr('y', 60)
.style('font-family', 'Rockwell')
.style('font-size',12)
.text('(Sentiwordnet');
main.append('svg:text')
.attr('x', width+7)
.attr('y', 75)
.style('font-family', 'Rockwell')
.style('font-size',12)
.text('sentiment score)');
// toggle tweets on and off
main.append('rect')
.attr('id','toggle')
.attr('x', width+7)
.attr('y', 88)
.attr('width',90)
.attr('height',20)
.style('fill','#4099FF')
.on('mouseup', switchAllData)
.attr('rx',5)
.attr('ry',5);
main.append('svg:text')
.attr('id', 'retweet-toggle')
.attr('x', width+15)
.attr('y', 102)
.text('Retweets: Off') // Toggles between on and off
.style('font-family', 'Rockwell')
.style('fill','white')
.style('font-size',12)
.style('pointer-events','none');
// draw the distanceBins for the mini chart
mini.append('g').selectAll('.laneLines')
.data(distanceBins)
.enter().append('line')
.attr('x1', 0)
.attr('y1', function(d,i) { return d3.round(y2(i)) + 0.5; })
.attr('x2', width)
.attr('y2', function(d,i) { return d3.round(y2(i)) + 0.5; })
.attr('stroke', function(d) { return d === '' ? 'white' : 'lightgray' });
mini.append('g').selectAll('.laneText')
.data(distanceBins)
.enter().append('svg:text')
.text(function(d) { return d[0]; })
.attr('x', -10)
.attr('y', function(d,i) { return y2(i+ 0.5); })
.attr('dy', '0.5ex')
.attr('text-anchor', 'end')
.attr('class', 'laneText');
// draw the x-axes
xDateAxis = d3.svg.axis()
.scale(x1)
.ticks(20)
.tickSize(15, 0, 0);
x1DateAxis = d3.svg.axis()
.scale(x)
.ticks(20)
.tickSize(15, 0, 0);
main.append('g')
.attr('transform', 'translate(0,' + mainHeight + ')')
.attr('class', 'main axis')
.call(xDateAxis);
mini.append('g')
.attr('transform', 'translate(0,125)')
.attr('class', 'axis')
.call(x1DateAxis)
.selectAll('text')
.attr('dx', 9)
.attr('dy', 12);
// declare the main rectangles to be amended in display()
itemRects = main.append('g')
.attr('clip-path', 'url(#clip)');
// draw mini graph
mini.append('g').selectAll('miniItems')
.data(switchData)
.enter().append('rect')
.attr('class', function(d) { return 'miniItem '; })
.attr("x",function(d,i) {return x(d.time_bin)})
.attr("y",function(d) {return y2(d.dist_bin+.08)})
.attr("width",x(2)-x(1))
.attr("height",y2(1)*.5)
.style('fill', function(d) {
var b = 255-(Math.round(colorScale(d.positivity)));
return "rgb(" + redOffset + "," + greenOffset + "," + b + ")";
});
// invisible rect for when brush is moved by click instead of drag
mini.append('rect')
.attr('pointer-events', 'painted')
.attr('width', width)
.attr('height', miniHeight)
.attr('visibility', 'hidden')
.on('mouseup', moveBrush);
// draw the selection area
brush = d3.svg.brush()
.x(x)
.extent([0,5])
.on("brush", display)
mini.append('g')
.attr('class', 'x brush')
.call(brush)
.selectAll('rect')
.attr('y', 1)
.attr('id','brushHover')
.attr('height', miniHeight - 1);
mini.selectAll('rect.background').remove();
display();
});
// check to see which dataset is currently displayed, and if button is clicked, switch dataset
function switchAllData() {
if (checkData == 0) {
main.select('#retweet-toggle')
.text('Retweets: On');
main.select('#toggle')
.transition().duration(500)
.style('fill','#009900');
checkData = 1;
switchData = jsonDataRTFilter;
} else {
main.select('#retweet-toggle')
.text('Retweets: Off');
main.select('#toggle')
.transition().duration(500)
.style('fill','#4099FF');
checkData = 0;
switchData = jsonDataFilter;
}
display();
}
function display () {
var rects, labels
, minExtent = brush.extent()[0]
, maxExtent = brush.extent()[1];
var extentWidth = maxExtent - minExtent;
if (extentWidth > max_extent_width)
maxExtent = minExtent + max_extent_width;
if (extentWidth < min_extent_width)
maxExtent = minExtent + min_extent_width;
var visItems = switchData.filter(function (d) {return d.time_bin <= maxExtent+1 && d.time_bin >= minExtent-1});
mini.select('.brush').call(brush.extent([minExtent, maxExtent]));
x1.domain([minExtent, maxExtent]);
if ((maxExtent - minExtent) >= 10) {
xDateAxis.ticks(10);
}
else {
xDateAxis.ticks(5);
}
// update the axis
main.select('.main.axis').call(xDateAxis)
.selectAll('text')
.attr('dx', 5)
.attr('dy', 12);
// upate the item rects
rects = itemRects.selectAll('rect')
.data(switchData, function (d,i) { return i; })
.attr('x', function(d) { return x1(d.time_bin); })
.attr('y', function(d) { return y1(d.dist_bin) + .1 * y1(1) + 0.5; })
.attr('width', 165);
rects.enter().append('svg:rect')
.attr('x', function(d) { return x1(d.time_bin); })
.attr('y', function(d) { return y1(d.dist_bin) + .1 * y1(1) + 0.5; })
.attr('width', 165)
.attr('class','borderMe')
.attr('height', function(d) { return .8 * y1(1); })
.style('fill', function(d) {
var b = 255-(Math.round(colorScale(d.positivity)));
return "rgb(" + redOffset + "," + greenOffset + "," + b + ")";
});
rects.exit().remove();
// update the item labels
labels = itemRects.selectAll('text')
.data(visItems, function (d) { return d.word; })
.text(function (d) { return d.word; })
.attr('x', function(d) {
return Math.round(x1(d.time_bin+0.05));
})
.attr('y', function(d) { return y1(d.dist_bin) + .6 * y1(1)})
.attr('id','hover')
.style('font-size', 14);
labels.enter().append('svg:text')
.text(function (d) { return d.word; })
.attr('x', function(d) {
return Math.round(x1(d.time_bin+0.05));
})
.attr('y', function(d) { return y1(d.dist_bin) + .6 * y1(1) })
.attr('text-anchor', 'start')
.attr('class', 'itemLabel')
.attr('id','hover')
.style('font-size', 14)
.style('font-family', 'Arial')
.style('fill','white')
.on('mouseup',function(d) {
var selectedWord = d.word;
highlightWords(selectedWord);
});
labels.order();
labels.exit().remove();
function highlightWords (a) {
mini.selectAll('miniItems')
.data(switchData)
.enter().append('rect')
.attr('class', function(d) { return 'miniItem '; })
.attr("x",function(d,i) {return x(d.time_bin)})
.attr("y",function(d) {return y2(d.dist_bin+.08)})
.attr("width",x(2)-x(1))
.attr("height",y2(1)*.5)
.style('fill', function(d) {
var b = Math.round(colorScale(d.positivity));
if (d.word == a) {
return "rgb(0,200,0)";
}
else {
var b = 255-(Math.round(colorScale(d.positivity)));
return "rgb(" + redOffset + "," + greenOffset + "," + b + ")";
}
})
rects = itemRects.selectAll('rect')
.data(switchData)
.transition().duration(1000)
.style('fill', function(d) {
var b = 255-(Math.round(colorScale(d.positivity)));
if (d.word == a) {
return "rgb(" + redOffset + ",200," + greenOffset + ")";
}
})
.transition().delay(2000).style('fill', function(d) {
var b = 255-(Math.round(colorScale(d.positivity)));
return "rgb(" + redOffset + "," + greenOffset + "," + b + ")";
});
}
// tipsy hover states by element ID
$('svg #hover').tipsy({
gravity: 'w',
html: true,
title: function() {
var d = this.__data__;
return d.sample_tweet;
}
});
$('svg #x_label').tipsy({
gravity: 'ne',
html: true,
title: function() {
return "0 Hour is LandFall";
}
})
$('svg #typeFace').tipsy({
gravity: 'ne',
html: true,
title: function() {
return "<span style='font-family: Arial; font-style: italic'>Italic is highly emotional,</span>" + "<br>" + "<span style='font-family: Arial'>No styling is a normal level of emotion.</span>" + "<br>" + "<span style='font-family: Arial Black; font-weight:bold'>Bold is removed of emotion.</span>";
}
})
$('svg #color').tipsy({
gravity: 'ne',
html: true,
title: function() {
return "Lighter shades correlate to higher positivity. (Sentiwordnet sentiment score)";
}
})
$('svg #frequency').tipsy({
gravity: 'ne',
html: true,
title: function() {
return "Larger fonts correlate to higher relative word frequency.";
}
})
$('svg #brushHover').tipsy({
gravity: 'sw',
html: true,
title: function() {
return "Drag or expand me to scroll through data.";
}
})
$('svg #toggle').tipsy({
gravity: 'ne',
html: true,
fontSize: 10,
title: function() {
return "Click here to add or remove retweets from the data.";
}
})
$('.tipsy:last').remove();
};
// for clicking brush to a location instead of dragging
function moveBrush () {
var origin = d3.mouse(this)
, point = x.invert(origin[0])
, halfExtent = (brush.extent()[1] - brush.extent()[0]) / 2
, start = point - halfExtent
, end = point + halfExtent;
brush.extent([start,end]);
display();
}
// Set up auto play parameters
var timer = null,
duration = 100,
extentIncrement = 0.05,
extentMax = 164;
// Define behavior of the Play button
$('#play').click(function() {
var extent = brush.extent();
if (extent[1] >= extentMax) {
extent[1] -= extent[0];
extent[0] = 0;
brush.extent(extent);
}
display();
if (timer) {
clearInterval(timer);
}
timer = setInterval(function() {
var extent = brush.extent();
if (extent[1] <= extentMax) {
extent[0] += extentIncrement;
extent[1] += extentIncrement;
display();
brush.extent(extent);
}
else {
$('#stop').click();
}
}, duration);
$(this).hide();
$('#stop').show();
});
$('#stop').click(function() {
if (timer) {
clearInterval(timer);
}
$(this).hide();
$('#play').show();
});
</script>
</body>
</html>

Twitter Word River

A d3.js visualization showing trending Twitter words during natural catastrophes by distance from the epicenter and over time.

Built on top of bunkat's Swimlane Chart: http://bl.ocks.org/1962173

This file has been truncated, but you can view the full file.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

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