Skip to content

Instantly share code, notes, and snippets.

@mike-ward
Last active December 25, 2015 19:49
Show Gist options
  • Save mike-ward/7030740 to your computer and use it in GitHub Desktop.
Save mike-ward/7030740 to your computer and use it in GitHub Desktop.
Visualization of Code Mash Sessions in D3js
<!DOCTYPE html>
<html>
<head>
<title>Code Mash Sessions</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.3.0/pure-min.css">
<script src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js" charset="utf-8"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.2.1/lodash.min.js"></script>
<style type="text/css">
body { overflow: hidden; margin: 0; font-size: 14px; font-family: "Helvetica Neue", Helvetica, sans-serif; background: #eee; }
#loading { text-align: center; font-size:medium; }
#header { font-size: 36px; font-weight: 300; text-shadow: 0 1px 0 #fff; text-align:center; width: 1400px; margin-top: 1pc; }
rect { fill: none; pointer-events: all; }
pre { font-size: 18px; }
line { stroke: #000; stroke-width: 1.5px; }
.string, .regexp { color: #f39; }
.keyword { color: #00c; }
.comment { color: #777; font-style: oblique; }
.number { color: #369; }
.class, .special { color: #1181B8; }
a:link, a:visited { color: #000; text-decoration: none; }
a:hover { color: #666; }
.hint { font-size: 12px; color: #999; }
.node circle { cursor: pointer; fill: #fff; stroke: steelblue; stroke-width: 1.5px; }
.node text { font-size: 11px; cursor: pointer; font-weight: bold; }
.node text:hover { fill: olivedrab; }
.legend circle { fill: #fff; stroke: steelblue; stroke-width: 1.5px; }
.legend text { font-size: 11px; font-weight: bold; }
path.link { fill: none; stroke: #ccc; stroke-width: 1.5px; }
.pure-button-close { border-radius: 5px; }
</style>
<style type="text/css">
div.overdiv { filter: alpha(opacity=75); opacity: .75; background-color: #333; position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; }
div.square { position: absolute; top: 20%; left: 35%; background-color: #fff; opacity: 1; border: thin solid #777; width: 40em; z-index: 10; border-radius: 5px; }
div.square div.msg { color: #777; font-size: 12pt; padding: 1pc; line-height: 1.3em; }
div.square button { float: right; margin-bottom: 1pc; margin-right: 1pc; }
div.square span { width: 7em; float: left; }
</style>
</head>
<body>
<div id="body">
<div id="header">
CodeMash Sessions<br>by <a id="filter" href="#" onclick="toggleFilter()">Technology</a>
<div class="hint">
<a style="color: #999" href="http://mike-ward.net" target="_blank">http://mike-ward.net</a>
</div>
<div>
<svg style="margin-top: 1pc;" width="400" height="20">
<g class="legend">
<circle cx="50" cy="10" style="fill: lawngreen" r="4.5"></circle>
<text x="60" y="13" text-anchor="start">Beginner</text>
<circle cx="140" cy="10" style="fill: gold" r="4.5"></circle>
<text x="150" y="13" text-anchor="start">Intermediate</text>
<circle cx="250" cy="10" style="fill: red" r="4.5"></circle>
<text x="260" y="13" text-anchor="start">Advanced</text>
</g>
</svg>
</div>
<div id="loading">
<div>Loading</div>
<img src="http://mike-ward.net/content/images/ajax-loader.gif">
</div>
</div>
</div>
<script type="text/javascript">
var codemashData;
var technologyFilter = 'Technology';
var timeFilter = 'Time';
var currentFilter = technologyFilter;
function toggleFilter() {
currentFilter = currentFilter === technologyFilter ? timeFilter : technologyFilter;
d3.select('#filter').text(currentFilter);
chart(currentFilter);
}
var chart = function (by) {
var m = [20, 120, 20, 120],
w = 1280 - m[1] - m[3],
h = 800 - m[0] - m[2],
i = 0,
root;
var tree = d3.layout.tree()
.size([h, w]);
var diagonal = d3.svg.diagonal()
.projection(function (d) { return [d.y, d.x]; });
d3.select('#chart').remove();
var vis = d3.select("#body").append("svg:svg")
.attr('id', 'chart')
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
function sortObj(obj) {
var keys = [];
var sorted = {};
for (var o in obj) {
if (obj.hasOwnProperty(o)) { keys.push(o) };
}
for (var i in keys.sort()) {
sorted[keys[i]] = obj[keys[i]];
}
return sorted;
}
var formatTitle = function (d) { return d.Title + ' (' + d.SpeakerName + ')'; };
var sortByDifficulty = function (items) {
return _.sortBy(items, function (s) {
switch (s.difficulty) {
case 'Beginner': return 1;
case 'Intermediate': return 2;
default: return 3;
}
});
};
var byTechnology = function (data) {
var technologies = _.groupBy(data, function (g) { return g.Technology });
technologies = sortObj(technologies);
var sessions = { 'name': 'Technology', 'children': [] };
_.each(technologies, function (sess, name) {
var items = _.map(sess, function (d) { return { 'name': formatTitle(d), 'difficulty': d.Difficulty, 'uri': d.URI }; });
items = sortByDifficulty(items);
var item = { 'name': name, 'children': items };
sessions.children.push(item);
});
return sessions;
}
var byTime = function (data) {
var formatName = function (d) { return d.Title + ' (' + d.SpeakerName + ')'; };
var days = _.groupBy(data, function (g) {
return g.Start ? moment(g.Start).dayOfYear() : undefined;
})
days = sortObj(days);
var tree = { 'name': 'Day', 'children': [] };
_.each(days, function (d, n) {
if (n === 'undefined') return;
var day = { 'name': moment(d[0].Start).format('ddd, MMM Do'), 'children': [] };
var hours = _.groupBy(d, function (h) { return h.Start });
hours = sortObj(hours);
_.each(hours, function (h, hh) {
var sessions = _.map(h, function (s) { return { 'name': formatTitle(s), 'difficulty': s.Difficulty, 'uri': s.URI }; });
sessions = sortByDifficulty(sessions);
day.children.push({ 'name': new Date(hh).toLocaleTimeString().replace(':00 ', ' '), 'children': sessions });
});
tree.children.push(day);
});
return tree;
};
root = (by === technologyFilter) ? byTechnology(codemashData) : byTime(codemashData);
root.x0 = h / 2;
root.y0 = 0;
function toggleAll(d) {
if (d.children) {
d.children.forEach(toggleAll);
toggle(d);
}
}
function toggleByName(name) {
var node = _.find(root.children, { 'name': name });
if (node) {
toggle(node);
toggle(node.children[0]);
}
}
// Initialize the display to show a few nodes.
root.children.forEach(toggleAll);
toggleByName('Cool Stuff');
toggleByName('Testing');
update(root);
function update(source) {
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse();
// Normalize for fixed-depth.
nodes.forEach(function (d) { d.y = d.depth * 180; });
// Update the nodes…
var node = vis.selectAll("g.node")
.data(nodes, function (d) { return d.id || (d.id = ++i); });
var difficultyToColor = function (difficulty) {
if (difficulty === 'Beginner') return 'lawngreen';
if (difficulty === 'Intermediate') return 'gold';
if (difficulty === 'Advanced') return 'red';
return '#fff';
};
function showDate(date) {
return date ? moment(date).format('ddd h:mmA') : 'not specified';
}
var showSession = function (title, uri) {
var pop = new Popup();
pop.popOut('<p>' + title + '</p><p><img src="http://mike-ward.net/content/images/ajax-loader.gif"></p>');
d3.json('http://rest.codemash.org' + uri, function (error, data) {
var message = (error)
? '<p>Aw, Snap!</p><p>' + error + '</p>'
: '<p><strong>' + data[0].Title + '</strong></p>' +
'<div><span>Speaker: </span><strong>' + data[0].SpeakerName + '</strong></div>' +
'<div><span>Technology: </span>' + data[0].Technology + '</div>' +
'<div><span>Difficulty: </span>' + data[0].Difficulty + '</div>' +
'<div><span>Time: </span>' + showDate(data[0].Start) + '</div>' +
'<p>' + data[0].Abstract.replace(/\n/g, '<br>') + '</p>';
pop.text(message);
});
}
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function (d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", function (d) {
if (d.uri) {
showSession(d.name, d.uri);
}
else {
toggle(d);
update(d);
}
});
nodeEnter.append("svg:circle")
.attr("r", 1e-6)
.style("fill", function (d) { return d._children ? "lightsteelblue" : difficultyToColor(d.difficulty); });
nodeEnter.append("svg:text")
.attr("x", function (d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function (d) { return d.children || d._children ? "end" : "start"; })
.text(function (d) { return d.name; })
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function (d) { return d._children ? "lightsteelblue" : difficultyToColor(d.difficulty); });
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) { return "translate(" + source.y + "," + source.x + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = vis.selectAll("path.link")
.data(tree.links(nodes), function (d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("svg:path", "g")
.attr("class", "link")
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
})
.transition()
.duration(duration)
.attr("d", diagonal);
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function (d) {
var o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children.
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
}
else {
d.children = d._children;
d._children = null;
}
}
function Popup() {
var msg;
var self = this;
this.square = null;
this.overdiv = null;
this.popOut = function (text) {
this.overdiv = document.createElement("div");
this.overdiv.className = "overdiv";
this.overdiv.addEventListener('mousedown', function () { self.popIn(); }, false);
this.square = document.createElement("div");
this.square.className = "square";
msg = document.createElement("div");
msg.className = "msg";
msg.innerHTML = text;
this.square.appendChild(msg);
var closebtn = document.createElement("button");
closebtn.className = 'pure-button pure-button-close';
closebtn.onclick = function () { self.popIn(); };
closebtn.tabIndex = 0;
closebtn.innerHTML = "Close";
this.square.appendChild(closebtn);
document.body.appendChild(this.overdiv);
document.body.appendChild(this.square);
closebtn.focus();
};
this.popIn = function () {
if (this.square) {
document.body.removeChild(this.square);
this.square = null;
}
if (this.overdiv) {
document.body.removeChild(this.overdiv);
this.overdiv = null;
}
};
this.text = function (text) {
msg.innerHTML = text;
};
}
};
d3.json("http://rest.codemash.org/api/sessions.jsonp", function (error, data) {
if (error) {
d3.select('#loading').text(error);
return;
}
codemashData = data;
d3.select('#loading').style('display', 'none');
chart(currentFilter);
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment