Skip to content

Instantly share code, notes, and snippets.

@kiran
Last active December 10, 2015 01:08
Show Gist options
  • Save kiran/4356033 to your computer and use it in GitHub Desktop.
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.
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