|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> |
|
<style> |
|
* { |
|
font-family: Helvetica; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<script> |
|
// taken directly from nbremer's occupationcanvas code |
|
//Generates the next color in the sequence, going from 0,0,0 to 255,255,255. |
|
//From: https://bocoup.com/weblog/2d-picking-in-canvas |
|
var nextCol = 1; |
|
function genColor(){ |
|
var ret = []; |
|
// via http://stackoverflow.com/a/15804183 |
|
if(nextCol < 16777215){ |
|
ret.push(nextCol & 0xff); // R |
|
ret.push((nextCol & 0xff00) >> 8); // G |
|
ret.push((nextCol & 0xff0000) >> 16); // B |
|
|
|
nextCol += 100; // This is exagerated for this example and would ordinarily be 1. |
|
} |
|
var col = "rgb(" + ret.join(',') + ")"; |
|
return col; |
|
} |
|
|
|
d3.json('twitter_profile3.json', function(image) { |
|
d3.json('tweets.json', function(tweets) { |
|
// some defaults for the image |
|
var imageSize = Math.sqrt(image.length); |
|
var scaleFactor = Math.floor(500 / imageSize); |
|
var data = []; |
|
var threshold = 158; |
|
var padding = 20; |
|
|
|
// set up canvas and hidden canvas |
|
var canvas = d3.select('body').append('canvas') |
|
.on('mousemove', mousemove).node(); |
|
canvas.width = imageSize * scaleFactor; |
|
canvas.height = imageSize * scaleFactor; |
|
var ctx = canvas.getContext('2d'); |
|
var hiddenCanvas = d3.select('body').append('canvas') |
|
.style('display', 'none') |
|
.node(); |
|
hiddenCanvas.width = imageSize * scaleFactor; |
|
hiddenCanvas.height = imageSize * scaleFactor; |
|
var hiddenCtx = hiddenCanvas.getContext('2d'); |
|
// then set up where tweet text will be shown |
|
var tweetDiv = d3.select('body').append('div') |
|
.style({ |
|
'width': (window.innerWidth - canvas.width - 3 * padding) + 'px', |
|
'display': 'inline-block', |
|
'vertical-align': 'top', |
|
'padding': padding + 'px' |
|
}); |
|
|
|
// first process the tweets |
|
var minOpacity = _.min(tweets, function(tweet) { |
|
return tweet.stats.favorites; |
|
}); |
|
minOpacity = minOpacity.stats.favorites + 1; |
|
var maxOpacity = _.max(tweets, function(tweet) { |
|
return tweet.stats.favorites; |
|
}); |
|
maxOpacity = maxOpacity.stats.favorites + 1; |
|
var opacityScale = d3.scale.log() |
|
.domain([minOpacity, maxOpacity]) |
|
.range([.25, 1]); |
|
var tweetColors = { |
|
'reply': [248,148,6], // orange |
|
'retweet': [81,163,81], // green |
|
'tweet': [0,136,204] // blue |
|
}; |
|
var colToTweet = {}; |
|
tweets = _.chain(tweets) |
|
.sortBy(function(tweet) { |
|
tweet.date = new Date(tweet.created_at); |
|
tweet.opacity = opacityScale(tweet.stats.favorites + 1); |
|
if (tweet.retweet || tweet.quote) { |
|
tweet.type = 'retweet'; |
|
} else if (tweet.in_reply_to) { |
|
tweet.type = 'reply'; |
|
} else { |
|
tweet.type = 'tweet'; |
|
} |
|
// and then remember the tweet by its unique color |
|
tweet.uniqColor = genColor(); |
|
colToTweet[tweet.uniqColor] = tweet; |
|
return tweet.date; |
|
}).sortBy(function(tweet, i) { |
|
tweet.index = i; |
|
return -tweet.date; |
|
}).value(); |
|
|
|
// turn it grayscale first |
|
_.each(image, function(pixel) { |
|
data.push(Math.max(pixel[0], pixel[1], pixel[2])); |
|
}); |
|
// Atkinson dithering |
|
var tweetIndex = 0; |
|
var tweetMap = {}; |
|
_.each(data, function(oldPixel, i) { |
|
var newPixel = oldPixel > threshold ? 255 : 0; |
|
var error = (oldPixel - newPixel) >> 3; |
|
|
|
data[i] = newPixel; |
|
data[i + 1] += error; |
|
data[i + 1] += error; |
|
data[i + imageSize - 1] += error; |
|
data[i + imageSize] += error; |
|
data[i + imageSize + 1] += error; |
|
data[i + imageSize + 2] += error; |
|
|
|
if (!newPixel) { |
|
// if the pixel is black, then keep track of |
|
// its corresponding tweet |
|
tweetMap[i] = tweets[tweetIndex]; |
|
tweetIndex += 1; |
|
} |
|
}); |
|
data = data.slice(0, imageSize * imageSize); |
|
drawCanvas(); |
|
|
|
function drawCanvas() { |
|
//first clear canvas |
|
ctx.fillStyle = "#fff"; |
|
ctx.rect(0, 0, canvas.width, canvas.height); |
|
ctx.fill(); |
|
|
|
_.each(data, function(pixel, i) { |
|
var tweet = tweetMap[i]; |
|
if (tweet) { |
|
var x = (i % imageSize) * scaleFactor + scaleFactor / 2; |
|
var y = Math.floor(i / imageSize) * scaleFactor + scaleFactor / 2; |
|
|
|
// first fill the visible canvas |
|
ctx.fillStyle = 'rgba(' + tweetColors[tweet.type].join(',') + |
|
',' + tweet.opacity + ')'; |
|
ctx.beginPath(); |
|
ctx.arc(x, y, scaleFactor * tweet.opacity, 0, 2 * Math.PI, true); |
|
ctx.fill(); |
|
if (tweet.hovered) { |
|
// if it's hovered, give it a stroke |
|
ctx.strokeStyle = 'rgb(255,216,75)'; |
|
ctx.lineWidth = 3; |
|
ctx.stroke(); |
|
} |
|
|
|
// then the hidden canvas |
|
hiddenCtx.fillStyle = tweet.uniqColor; |
|
hiddenCtx.beginPath(); |
|
hiddenCtx.fillRect(x - scaleFactor / 2, y - scaleFactor / 2, |
|
scaleFactor, scaleFactor); |
|
} |
|
}); |
|
} |
|
|
|
var currentTweet; |
|
var dateFormat = d3.time.format("%Y-%m-%d"); |
|
function mousemove() { |
|
var col = hiddenCtx.getImageData(d3.event.offsetX, d3.event.offsetY, 1, 1).data; |
|
var color = 'rgb(' + col[0] + "," + col[1] + ","+ col[2] + ")"; |
|
var tweet = colToTweet[color]; |
|
|
|
// we only want to re-render if hovered tweet is different from current tweet |
|
if (tweet && (!currentTweet || tweet.id !== currentTweet.id)) { |
|
// first clean up currentTweet (now previous tweet) |
|
if (currentTweet) { |
|
currentTweet.hovered = false; |
|
} |
|
currentTweet = tweet; |
|
|
|
tweet.hovered = true; |
|
drawCanvas(); |
|
|
|
var tweetString = '<strong>tweet #' + tweet.index + '</strong>: '; |
|
tweetString += dateFormat(tweet.date) + '</br>'; |
|
tweetString += '<p>' + tweet.text + '</p>'; |
|
tweetString += '<p>' + tweet.stats.favorites + ' favorites, ' + |
|
tweet.stats.retweets + ' retweets</p>'; |
|
tweetDiv.html(tweetString); |
|
} |
|
} |
|
}); |
|
}); |
|
</script> |
|
</body> |