Last active
October 28, 2016 13:32
-
-
Save somiandras/5fdb594ddbd86db1e9d79e3e3ade4775 to your computer and use it in GitHub Desktop.
Bubble (pack) chart for Stocktwits stock mentions.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>D3 test</title> | |
<style> | |
body { | |
font-family: Arial; | |
color: #888888; | |
} | |
</style> | |
<script src="https://d3js.org/d3.v4.js"></script> | |
</head> | |
<body> | |
<div> | |
<button id="refresh">REDRAW</button> | |
</div> | |
<svg height="600"></svg> | |
<script src="stocktwits_bubbles.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(window, document) { | |
'use strict'; | |
let refresh = document.getElementById('refresh'); | |
window.onload = function() { | |
d3.json('data.json', d => { | |
extractData(d); | |
refresh.addEventListener('click', () => extractData(d)); | |
}); | |
}; | |
function extractData(messages) { | |
let data = {}; | |
messages.forEach(message => { | |
if (message.symbols) { | |
let symbols = message.symbols; | |
symbols.forEach(symbol => { | |
if (data[symbol.symbol]) { | |
data[symbol.symbol].count += 1; | |
} else { | |
data[symbol.symbol] = { | |
count: 1, | |
title: symbol.title | |
}; | |
} | |
}); | |
} | |
}); | |
drawPack(arrayify(data)); | |
let textData = { | |
messages: messages.length, | |
minDate: d3.min(messages, d => new Date(d.created_at)), | |
maxDate: d3.max(messages, d => new Date(d.created_at)) | |
}; | |
addTexts(textData); | |
} | |
function arrayify(data) { | |
let list = Object.keys(data); | |
let array = list.map(item => { | |
return { | |
name: item, | |
count: data[item].count, | |
title: data[item].title | |
}; | |
}); | |
return array; | |
} | |
// THE DATA VISUALIZATON STARTS HERE | |
function drawPack(data, textData) { | |
// Remove bubbles and caption before creating any new | |
d3.select('svg').selectAll('g').remove(); | |
d3.selectAll('.caption').remove(); | |
// Create hierarchy from data | |
let nodes = { | |
children: data | |
}; | |
let root = d3.hierarchy(nodes) | |
.sum(d => d.count); | |
// Create pack layout | |
let diameter = 600; | |
let pack = d3.pack().size([diameter, diameter]).padding(2); | |
// Color scale | |
let circleColor = d3.scaleLog() | |
.domain([d3.min(nodes.children, d => d.count), | |
d3.max(nodes.children, d => d.count)]) | |
.range(['#9FC3FF', '#2F2BAD']) | |
.interpolate(d3.interpolateHsl); | |
// Scale for circles' movement | |
let yScale = d3.scaleLog() | |
.domain([d3.min(nodes.children, d => d.count), | |
d3.max(nodes.children, d => d.count)]) | |
.range([10, 150]); | |
// Root svg | |
let svg = d3.select('svg') | |
.attr('width', 900) | |
.attr('height', diameter + 100); | |
// Data enter | |
let node = svg.selectAll('.node') | |
.data(pack(root).leaves()) | |
.enter() | |
.append('g') | |
.attr('class', 'node') | |
.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')'); | |
// Add bubbles | |
node.append('circle') | |
.style('fill', d => circleColor(d.data.count)) | |
.attr('cy', d => yScale(d.data.count)) | |
.style('opacity', 0) | |
.transition() | |
.duration(600) | |
.attr('cy', 0) | |
.style('opacity', 1) | |
.delay((d, i) => { | |
let delay; | |
if (d.r > 30) { | |
delay = i * 10; | |
} else { | |
delay = i * 5; | |
} | |
return delay; | |
}) | |
.ease(d3.easeBackOut) | |
.attr('r', d => d.r) | |
.call(endAll, attachListeners); | |
// Wait for the end of all bubble transitions | |
function endAll(transition, callback) { | |
if (!callback) callback = function() {}; | |
if (transition.size === 0) { | |
callback(); | |
} | |
let n = 0; | |
transition | |
.each(() => ++n) | |
.on('end', function() { | |
if (!--n) { | |
callback.apply(this, arguments); | |
} | |
}); | |
} | |
// Bubble titles | |
node | |
.append('text') | |
.attr('dy', '.3em') | |
.style('opacity', 0) | |
.style('text-anchor', 'middle') | |
.style('font-family', 'arial') | |
.style('font-size', d => { | |
return Math.floor(d.r / 20) + 10; | |
}) | |
.style('fill', '#ffffff') | |
.style('pointer-events', 'none') | |
.filter(d => d.r > 20) | |
.text(d => d.data.name) | |
.transition() | |
.duration(1000) | |
.delay((d, i) => i * 40) | |
.style('opacity', 1); | |
// Events | |
function attachListeners() { | |
node.on('mouseover', function(d) { | |
d3.select(this).select('circle') | |
.style('stroke-width', 3) | |
.style('stroke', 'orange'); | |
let legend = svg.append('g') | |
.attr('class', 'legend') | |
.attr('transform', 'translate(560, 30)'); | |
legend.append('text') | |
.style('text-anchor', 'left') | |
.style('font-family', 'arial') | |
.style('font-size', 12) | |
.style('fill', '#888888') | |
.text(() => d.data.title); | |
let legendText = legend.append('text') | |
.attr('y', 22) | |
.style('text-anchor', 'left') | |
.style('font-family', 'arial') | |
.style('fill', '#888888') | |
.style('font-size', 12); | |
legendText.append('tspan') | |
.style('font-weight', 'bold') | |
.style('font-size', 20) | |
.text(() => d.data.count); | |
legendText.append('tspan') | |
.text(' mentions'); | |
}); | |
node.on('mouseout', function(d) { | |
d3.select(this).select('circle') | |
.transition() | |
.duration(200) | |
.style('stroke', 'none'); | |
svg.select('.legend').remove(); | |
}); | |
} | |
} | |
// Add caption | |
function addTexts(textData) { | |
let caption = `Cashtag mentions from ${textData.messages} | |
StockTwits trending messages between ${textData.minDate.toLocaleString()} | |
and ${textData.maxDate.toLocaleString()}.`; | |
let svg = d3.select('svg'); | |
svg.append('text').attr('class', 'caption') | |
.attr('transform', 'translate(' + 0 + ',' + (svg.attr('height') - 60) + ')') | |
.attr('y', 50) | |
.style('fill', '#888888') | |
.style('opacity', 0) | |
.style('font-size', '13') | |
.transition() | |
.duration(1300) | |
.delay(1500) | |
.ease(d3.easeCubicOut) | |
.attr('y', 0) | |
.style('opacity', 1) | |
.text(caption); | |
} | |
})(window, document); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment