Last active
December 10, 2015 01:08
-
-
Save kiran/4356033 to your computer and use it in GitHub Desktop.
JS behind the MIT Tech's stressful classes visualization.
Data not available for privacy reasons.
This file contains hidden or 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
var BubbleChart = (function () { | |
function BubbleChart (data) { | |
if (this == window) { | |
return new BubbleChart(data); | |
} | |
this.data = data; | |
this.width = 940; | |
this.height = 800; | |
this.format = d3.format(",d"); | |
this.tooltip = new CustomTooltip("classes_tooltip", 240); | |
this.fill_color = d3.scale.ordinal() | |
.domain(["Architecture and Planning", "Engineering", | |
"Humanities, Arts, and Social Sciences", "Sloan School of Management", "Science", "Thesis"]) | |
.range(["#D9BA91", "#4998DE", "#FFE0BA", "#E5AB17", "#aec7e8", "#398589"]); | |
this.center = {x: this.width / 2, y: this.height / 2}; | |
this.school_centers = { | |
"Engineering": {x: this.width*( 1/ 2 + 1/14), y: this.height / 2}, | |
"Science": {x: this.width*( 1/ 2 - 1/14), y: this.height / 2}, | |
"Architecture and Planning": {x: this.width*( 1/ 2 ), y: this.height*( 1/ 2 + 1/14)}, | |
"Humanities, Arts, and Social Sciences": {x: this.width*( 1/ 2 ), y: this.height*( 1/ 2 - 1/15)}, | |
"Sloan School of Management": {x: this.width*( 1/ 2 ), y: this.height*( 1/ 2 )}, | |
"Thesis": {x: this.width*( 1/ 2 ), y: this.height*( 1/ 2 + 1/15)} | |
}; | |
// used when setting up force and moving around nodes | |
this.layout_gravity = -0.01; | |
this.damper = 0.1; | |
this.force = null; | |
// use the max total_amount in the data as the max in the scale's domain | |
var max_amount = 100; | |
this.radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 85]); | |
// these will be set in create_nodes and create_vis | |
this.vis = null; | |
this.nodes = []; | |
this.circles = null; | |
this.create_nodes(); | |
this.create_vis(); | |
return this; | |
} | |
BubbleChart.prototype.create_nodes = function () { | |
var that = this; | |
this.parsed = {}; | |
this.data.forEach(function (d) { | |
that.parsed[d.subject] = d; | |
var i = 0, | |
value = parseInt(d.Freshmen,10)+parseInt(d.Sophomores,10)+parseInt(d.Juniors,10)+parseInt(d.Seniors,10) | |
+parseInt(d['Fifth year or above'],10)+parseInt(d["Graduate (Master's)"],10)+parseInt(d["Graduate (PhD)"],10), | |
node = { | |
id: i, | |
value: value, | |
radius: that.radius_scale(parseInt(value,10)), | |
subject: d.subject, | |
school: d.school, | |
course: d.course, | |
x: that.school_centers[d.school].x + Math.random()*that.width/5, | |
y: that.school_centers[d.school].y + Math.random()*that.height/5 | |
}; | |
i++; | |
that.nodes.push(node); | |
}); | |
this.nodes.sort( function (a,b) { return b.value - a.value; }); | |
}; | |
BubbleChart.prototype.create_vis = function () { | |
this.vis = d3.select("#chart").append("svg") | |
.attr("width", this.width) | |
.attr("height", this.height) | |
.attr("id", "svg_vis"); | |
this.circles = this.vis.selectAll("g.node") | |
.data(this.nodes, function(d) {return d.id;}) | |
.enter().append("svg:g") | |
.attr("class", "node"); | |
// this.circles = this.vis.selectAll("circle") | |
// .data(this.nodes, function(d) {return d.id;}); | |
// used because we need 'this' in the | |
// mouse callbacks | |
var that = this; | |
// radius will be set to 0 initially. | |
// see transition below | |
this.circles.append("circle") | |
.attr("r", 0) | |
.attr("fill", function(d) { return that.fill_color(d.school); }) | |
.attr("stroke-width", 1) | |
.attr("stroke", function(d) { return d3.rgb(that.fill_color(d.school)).darker(); }) | |
.attr("id", function(d) { return "bubble_"+d.id; }) | |
.on("mouseover", function (d,i) { return that.show_details(d,i,this,that); }) | |
.on("mouseout", function (d,i) { return that.hide_details(d,i,this,that); }); | |
// Fancy transition to make bubbles appear, ending with the | |
// correct radius | |
this.circles.selectAll("circle").transition().duration(1000).attr("r", function (d) { return d.radius; }); | |
this.circles.append("text") | |
.attr("text-anchor", "middle") | |
.attr("dy", ".3em") | |
.text(function(d) { if (d.subject) { return d.subject.substring(0, d.radius / 3);} return ''; }); | |
}; | |
// Charge function that is called for each node. | |
// Charge is proportional to the diameter of the | |
// circle (which is stored in the radius attribute | |
// of the circle's associated data. | |
// This is done to allow for accurate collision | |
// detection with nodes of different sizes. | |
// Charge is negative because we want nodes to | |
// repel. | |
// Dividing by 8 scales down the charge to be | |
// appropriate for the visualization dimensions. | |
BubbleChart.prototype.charge = function(d) { | |
return -Math.pow(d.radius, 2.0) / 8; | |
}; | |
// Starts up the force layout with | |
// the default values | |
BubbleChart.prototype.start = function() { | |
this.force = d3.layout.force() | |
.nodes(this.nodes) | |
.size([this.width, this.height]); | |
return this.force; | |
}; | |
// Sets up force layout to display | |
// all nodes in one circle. | |
BubbleChart.prototype.start_nodes = function() { | |
var that = this; | |
that.force.gravity(that.layout_gravity) | |
.charge(this.charge) | |
.friction(0.9) | |
.on("tick", function(e) { | |
that.circles.each(that.move_towards_center(e.alpha)) | |
.attr("transform", function(d) { return "translate(" + d.x + ","+ d.y + ")"; }) | |
}); | |
that.force.start() | |
}; | |
// Moves all circles towards the @center | |
// of the visualization | |
BubbleChart.prototype.move_towards_center = function (alpha){ | |
var that = this; | |
return function (d) { | |
var target = that.school_centers[d.school] || that.center; | |
d.x = d.x + (target.x - d.x) * (that.damper + 0.02) * alpha; | |
d.y = d.y + (target.y - d.y) * (that.damper + 0.02) * alpha; | |
}; | |
}; | |
BubbleChart.prototype.updateFilters = function(checked, courses) { | |
var years = Object.keys(checked), | |
that = this; | |
this.nodes.forEach(function (node) { | |
var count = 0; | |
years.forEach (function (y) { | |
count += parseInt(that.parsed[node.subject][y],10); | |
}); | |
node.value = count; | |
node.radius = that.radius_scale(count); | |
if (count == 0) node.radius = 0; | |
if (!courses[node.course]) node.radius = 0; | |
}); | |
// Fancy transition to make bubbles appear, ending with the | |
// correct radius | |
this.circles | |
.selectAll("circle") | |
.transition() | |
.duration(500) | |
.attr("r", function (d) { return d.radius; }); | |
this.circles | |
.selectAll("text") | |
.text(function(d) { if (d.subject) { return d.subject.substring(0, d.radius / 3);} return ''; }); | |
this.start_nodes(); | |
}; | |
BubbleChart.prototype.show_details = function(data, i, element, that) { | |
d3.select(element).attr("stroke", "black"); | |
content = "<span class=\"name\">Subject: </span><span class=\"value\">" + data.subject + "</span><br/>"; | |
content +="<span class=\"name\">Amount: </span><span class=\"value\">" + data.value + "</span><br/>"; | |
content +="<span class=\"name\">School of </span><span class=\"value\">" + data.school + "</span><br/>"; | |
that.tooltip.showTooltip(content,d3.event); | |
}; | |
BubbleChart.prototype.hide_details = function(data, i, element, that) { | |
d3.select(element).attr("stroke", function(d) { return d3.rgb(that.fill_color(d.school)).darker(); }); | |
that.tooltip.hideTooltip() | |
}; | |
return BubbleChart; | |
})(); | |
$(document).ready(function() { | |
var chart = null; | |
var render_vis = function(csv) { | |
chart = new BubbleChart(csv); | |
chart.start(); | |
chart.start_nodes(); | |
}; | |
var formChanged = function () { | |
var years = {}, courses = {}; | |
$('form#year input[type=checkbox]:checked').each(function() { | |
years[this.value] = true; | |
}); | |
$('form#course input[type=checkbox]:checked').each(function() { | |
courses[this.value] = true; | |
}); | |
chart.updateFilters(years, courses); | |
}; | |
var selectAggregate = function (form, value) { | |
var selector = form+' input'; | |
return function() { | |
$(selector).each( function() { | |
$(this).prop('checked', value); | |
}); | |
formChanged(); | |
return false; | |
}; | |
}; | |
d3.csv("./data/data_parsed.csv", render_vis); | |
$("form#year").change(formChanged); | |
$("form#course").change(formChanged); | |
$(".button#select-none-years").click(selectAggregate('#year', false)); | |
$(".button#select-all-years").click(selectAggregate('#year', true)); | |
$(".button#select-none-courses").click(selectAggregate('#course', false)); | |
$(".button#select-all-courses").click(selectAggregate('#course', true)); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment