Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Hack Matrix
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.filter {
text-align: center;
font-size: 18px;
font-family: Georgia, serif;
}
.filter label {
margin-left: 10px;
}
.chart {
margin: 1em auto;
width: 940px;
height: 940px;
font-size: 20px;
}
.chart text {
alignment-baseline: middle;
}
.chart .grid {
stroke-width: 2px;
stroke: rgb(204,204,204);
fill: none;
}
.chart .axis {
stroke: none;
}
.chart .axis-label {
text-transform: uppercase;
font-family: Georgia, serif;
font-style: italic;
}
.chart .story {
fill: black;
}
.chart .story:hover {
fill: rgb(225,47,39);
}
.chart .story circle {
stroke: none;
}
.chart .story text {
font-family: Helvetica, sans-serif;
font-weight: bold;
}
</style>
<body>
<form class="filter">
Show only hacks that are: <label for="good">Good</label><input checked value="good" id="good" type="checkbox"> <label for="bad">Bad</label><input checked value="bad" id="bad" type="checkbox"><label for="murky">Murky</label><input checked value="murky" id="murky" type="checkbox"></form>
<div class="chart">
<img alt="" class="mobile-only" src="http://spectrum.ieee.org/img/1887209-1386619971976.jpeg" />
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="tabletop.js"></script>
<script>
window.onload = function() { init() };
var public_spreadsheet_url = 'https://docs.google.com/spreadsheets/d/1ngwScUbzGUoZ2cz3Q_OCijJzZlXQ_gu-IMZW3yzHkcI/pubhtml';
function init() {
Tabletop.init( { key: public_spreadsheet_url,
callback: showInfo,
simpleSheet: true } )
}
//Options
var margin = {top: 30, right: 30, bottom: 30, left: 30},
width = 940 - margin.left - margin.right,
height = 940 - margin.top - margin.bottom,
dotRadius = 8,
gridSpacing = 10
gridMax = 5;
var svg = d3.select(".chart").html("").append("svg")
.attr("width",width + margin.left + margin.right)
.attr("height",height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Scales for item positions
var x = d3.scale.linear().domain([-gridMax,gridMax]).range([0,width]);
var y = d3.scale.linear().domain([-gridMax,gridMax]).range([height,0]);
//gridlines
svg.append("path")
.attr("class","grid")
.attr("d",function() {
var d = "";
for (var i = -gridMax; i <= gridMax; i ++ ) {
d += "M"+x(i)+",0 L"+x(i)+","+height;
d += "M0,"+y(i)+" L"+width+","+y(i);
}
return d;
})
var defs = svg.append("defs");
var hgrad = defs.append("linearGradient")
.attr("id","gradh")
.attr("x1","0%")
.attr("y1","0%")
.attr("x2","100%")
.attr("y2","0%")
.attr("spreadMethod","pad");
var vgrad = defs.append("linearGradient")
.attr("id","gradv")
.attr("x1","0%")
.attr("y1","0%")
.attr("x2","0%")
.attr("y2","100%")
.attr("spreadMethod","pad");
hgrad.append("stop")
.attr("offset","0%")
.style("stop-color","rgb(0,152,224)")
.style("stop-opacity","1");
hgrad.append("stop")
.attr("offset","50%")
.style("stop-color","rgb(221,221,221)")
.style("stop-opacity","1");
hgrad.append("stop")
.attr("offset","100%")
.style("stop-color","rgb(225,47,39)")
.style("stop-opacity","1");
vgrad.append("stop")
.attr("offset","0%")
.style("stop-color","rgb(225,47,39)")
.style("stop-opacity","1");
vgrad.append("stop")
.attr("offset","50%")
.style("stop-color","rgb(221,221,221)")
.style("stop-opacity","1");
vgrad.append("stop")
.attr("offset","100%")
.style("stop-color","rgb(0,152,224)")
.style("stop-opacity","1");
//x axis
svg.append("rect")
.attr("class","axis")
.attr("x",0)
.attr("width",width)
.attr("y",(height/2)-4)
.attr("height",8)
.attr("fill","url(#gradh)")
.attr("stroke","none");
//y axis
svg.append("rect")
.attr("class","axis")
.attr("x",(width/2)-4)
.attr("width",8)
.attr("y",0)
.attr("height",height)
.attr("fill","url(#gradv)")
.attr("stroke","none");
//axis labels
var nLabel = svg.append("g")
.attr("transform","translate("+x(0)+","+y(gridMax)+")")
.attr("class","axis-label");
nLabel.append("rect")
.attr("x",-x(1 - gridMax))
.attr("width",x(2 - gridMax))
.attr("y",y(gridMax + .25))
.attr("height",y((gridMax-.5)))
.attr("fill","rgb(221,221,221)")
.attr("stroke","rgb(204,204,204)")
.attr("stroke-width",2)
nLabel.append("text")
.text("High Impact")
.attr("x",0)
.attr("y",0)
.attr("text-anchor","middle")
.style("alignment-baseline","middle");
var sLabel = svg.append("g")
.attr("transform","translate("+x(0)+","+y(-gridMax)+")")
.attr("class","axis-label");
sLabel.append("rect")
.attr("x",-x(1 - gridMax))
.attr("width",x(2 - gridMax))
.attr("y",y(gridMax + .25))
.attr("height",y((gridMax-.5)))
.attr("fill","rgb(221,221,221)")
.attr("stroke","rgb(204,204,204)")
.attr("stroke-width",2)
sLabel.append("text")
.text("Low Impact")
.attr("x",0)
.attr("y",0)
.attr("text-anchor","middle")
.style("alignment-baseline","middle");
var wLabel = svg.append("g")
.attr("transform","translate("+x(-gridMax)+","+y(0)+"),rotate(-90)")
.attr("class","axis-label");
wLabel.append("rect")
.attr("x",-x(1 - gridMax))
.attr("width",x(2 - gridMax))
.attr("y",y(gridMax + .25))
.attr("height",y((gridMax-.5)))
.attr("fill","rgb(221,221,221)")
.attr("stroke","rgb(204,204,204)")
.attr("stroke-width",2)
wLabel.append("text")
.text("Simple")
.attr("x",0)
.attr("y",0)
.attr("text-anchor","middle")
.style("alignment-baseline","middle");
var eLabel = svg.append("g")
.attr("transform","translate("+x(gridMax)+","+y(0)+"),rotate(90)")
.attr("class","axis-label");
eLabel.append("rect")
.attr("x",-x(1 - gridMax))
.attr("width",x(2 - gridMax))
.attr("y",y(gridMax + .25))
.attr("height",y((gridMax-.5)))
.attr("fill","rgb(221,221,221)")
.attr("stroke","rgb(204,204,204)")
.attr("stroke-width",2)
eLabel.append("text")
.text("Innovative")
.attr("x",0)
.attr("y",0)
.attr("text-anchor","middle")
.style("alignment-baseline","middle");
function showInfo(data, tabletop) {
//console.log(data);
//set default text alignment (compass directions: N, NE, E, SE, S, SW, W, NW)
data.forEach(function (d) {
if (!d.align) {
d.align = "S";
}
})
//One group per item
var items = svg.selectAll("g.item").data(data).enter().append("g")
.attr("class","item");
var links = items.append("a")
.attr("xlink:href",function(d){ return d.link; })
.attr("target","_parent")
.attr("class",function(d){ return "story "+ d.filter; })
.attr("transform",function(d){ return "translate("+x(+d.innovation)+","+y(+d.impact)+")";});
//Add a dot
links.append("circle")
.attr("r",dotRadius)
.attr("cx",0)
.attr("cy",0);
//Add text group
var label = links.append("g")
.attr("text-anchor",function (d){
if (Math.abs(gridMax - d.innovation) < 1 || d.align.match(/W/)) {
return "end";
}
else if (+d.innovation + gridMax < 1 || d.align.match(/E/)){
return "start";
}
else {
return "middle";
}
})
.attr("class",function(d){ return d.align; })
.attr("transform",function(d){
var x0 = 0,
y0 = 0,
textHeight = d.title.split(" ").length * 18;
if (d.align.match(/W/)) {
x0 = -(2 * dotRadius);
}
else if (d.align.match(/E/)) {
x0 = 2 * dotRadius;
}
if (d.align.match(/S/)) {
y0 = 18 + dotRadius - Math.abs(x0/2);
x0 = x0/2;
}
else if (d.align.match(/N/)) {
y0 = -textHeight - dotRadius + Math.abs(x0/2) ;
x0 = x0/2;
}
else {
y0 = (-textHeight * .5) + (18 / 2);
}
return "translate("+x0+","+y0+")";
});
//Add titles
label.selectAll("text")
.data(function (d) {
var title = d.title.split(" ");
return title;
})
.enter().append("text")
.attr("x",0)
.attr("y",0)
.attr("dy",function(d, i) {
return (i) + "em";
})
.text(function(d){return d;});
d3.selectAll(".filter input")
.on("change", function () {
var type = this.value;
display = this.checked ? "inline" : "none";
svg.selectAll(".story")
.filter(function(d) {
return d.filter === type; })
.attr("display", display);
});
}
</script>
(function(global) {
"use strict";
var inNodeJS = false;
if (typeof module !== 'undefined' && module.exports) {
inNodeJS = true;
var request = require('request');
}
var supportsCORS = false;
var inLegacyIE = false;
try {
var testXHR = new XMLHttpRequest();
if (typeof testXHR.withCredentials !== 'undefined') {
supportsCORS = true;
} else {
if ("XDomainRequest" in window) {
supportsCORS = true;
inLegacyIE = true;
}
}
} catch (e) { }
// Create a simple indexOf function for support
// of older browsers. Uses native indexOf if
// available. Code similar to underscores.
// By making a separate function, instead of adding
// to the prototype, we will not break bad for loops
// in older browsers
var indexOfProto = Array.prototype.indexOf;
var ttIndexOf = function(array, item) {
var i = 0, l = array.length;
if (indexOfProto && array.indexOf === indexOfProto) return array.indexOf(item);
for (; i < l; i++) if (array[i] === item) return i;
return -1;
};
/*
Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } )
OR!
Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } )
OR!
Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc')
*/
var Tabletop = function(options) {
// Make sure Tabletop is being used as a constructor no matter what.
if(!this || !(this instanceof Tabletop)) {
return new Tabletop(options);
}
if(typeof(options) === 'string') {
options = { key : options };
}
this.callback = options.callback;
this.wanted = options.wanted || [];
this.key = options.key;
this.simpleSheet = !!options.simpleSheet;
this.parseNumbers = !!options.parseNumbers;
this.wait = !!options.wait;
this.reverse = !!options.reverse;
this.postProcess = options.postProcess;
this.debug = !!options.debug;
this.query = options.query || '';
this.orderby = options.orderby;
this.endpoint = options.endpoint || "https://spreadsheets.google.com";
this.singleton = !!options.singleton;
this.simple_url = !!options.simple_url;
this.callbackContext = options.callbackContext;
if(typeof(options.proxy) !== 'undefined') {
// Remove trailing slash, it will break the app
this.endpoint = options.proxy.replace(/\/$/,'');
this.simple_url = true;
this.singleton = true;
// Let's only use CORS (straight JSON request) when
// fetching straight from Google
supportsCORS = false
}
this.parameterize = options.parameterize || false;
if(this.singleton) {
if(typeof(Tabletop.singleton) !== 'undefined') {
this.log("WARNING! Tabletop singleton already defined");
}
Tabletop.singleton = this;
}
/* Be friendly about what you accept */
if(/key=/.test(this.key)) {
this.log("You passed an old Google Docs url as the key! Attempting to parse.");
this.key = this.key.match("key=(.*?)&")[1];
}
if(/pubhtml/.test(this.key)) {
this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse.");
this.key = this.key.match("d\\/(.*?)\\/pubhtml")[1];
}
if(!this.key) {
this.log("You need to pass Tabletop a key!");
return;
}
this.log("Initializing with key " + this.key);
this.models = {};
this.model_names = [];
this.base_json_path = "/feeds/worksheets/" + this.key + "/public/basic?alt=";
if (inNodeJS || supportsCORS) {
this.base_json_path += 'json';
} else {
this.base_json_path += 'json-in-script';
}
if(!this.wait) {
this.fetch();
}
};
// A global storage for callbacks.
Tabletop.callbacks = {};
// Backwards compatibility.
Tabletop.init = function(options) {
return new Tabletop(options);
};
Tabletop.sheets = function() {
this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)");
};
Tabletop.prototype = {
fetch: function(callback) {
if(typeof(callback) !== "undefined") {
this.callback = callback;
}
this.requestData(this.base_json_path, this.loadSheets);
},
/*
This will call the environment appropriate request method.
In browser it will use JSON-P, in node it will use request()
*/
requestData: function(path, callback) {
if (inNodeJS) {
this.serverSideFetch(path, callback);
} else {
//CORS only works in IE8/9 across the same protocol
//You must have your server on HTTPS to talk to Google, or it'll fall back on injection
var protocol = this.endpoint.split("//").shift() || "http";
if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) {
this.xhrFetch(path, callback);
} else {
this.injectScript(path, callback);
}
}
},
/*
Use Cross-Origin XMLHttpRequest to get the data in browsers that support it.
*/
xhrFetch: function(path, callback) {
//support IE8's separate cross-domain object
var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest();
xhr.open("GET", this.endpoint + path);
var self = this;
xhr.onload = function() {
try {
var json = JSON.parse(xhr.responseText);
} catch (e) {
console.error(e);
}
callback.call(self, json);
};
xhr.send();
},
/*
Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data
it triggers the callback. This helps you avoid cross-domain errors
http://code.google.com/apis/gdata/samples/spreadsheet_sample.html
Let's be plain-Jane and not use jQuery or anything.
*/
injectScript: function(path, callback) {
var script = document.createElement('script');
var callbackName;
if(this.singleton) {
if(callback === this.loadSheets) {
callbackName = 'Tabletop.singleton.loadSheets';
} else if (callback === this.loadSheet) {
callbackName = 'Tabletop.singleton.loadSheet';
}
} else {
var self = this;
callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000));
// Create a temp callback which will get removed once it has executed,
// this allows multiple instances of Tabletop to coexist.
Tabletop.callbacks[ callbackName ] = function () {
var args = Array.prototype.slice.call( arguments, 0 );
callback.apply(self, args);
script.parentNode.removeChild(script);
delete Tabletop.callbacks[callbackName];
};
callbackName = 'Tabletop.callbacks.' + callbackName;
}
var url = path + "&callback=" + callbackName;
if(this.simple_url) {
// We've gone down a rabbit hole of passing injectScript the path, so let's
// just pull the sheet_id out of the path like the least efficient worker bees
if(path.indexOf("/list/") !== -1) {
script.src = this.endpoint + "/" + this.key + "-" + path.split("/")[4];
} else {
script.src = this.endpoint + "/" + this.key;
}
} else {
script.src = this.endpoint + url;
}
if (this.parameterize) {
script.src = this.parameterize + encodeURIComponent(script.src);
}
document.getElementsByTagName('script')[0].parentNode.appendChild(script);
},
/*
This will only run if tabletop is being run in node.js
*/
serverSideFetch: function(path, callback) {
var self = this
request({url: this.endpoint + path, json: true}, function(err, resp, body) {
if (err) {
return console.error(err);
}
callback.call(self, body);
});
},
/*
Is this a sheet you want to pull?
If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported
Pulls all sheets if none are specified
*/
isWanted: function(sheetName) {
if(this.wanted.length === 0) {
return true;
} else {
return (ttIndexOf(this.wanted, sheetName) !== -1);
}
},
/*
What gets send to the callback
if simpleSheet === true, then don't return an array of Tabletop.this.models,
only return the first one's elements
*/
data: function() {
// If the instance is being queried before the data's been fetched
// then return undefined.
if(this.model_names.length === 0) {
return undefined;
}
if(this.simpleSheet) {
if(this.model_names.length > 1 && this.debug) {
this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong.");
}
return this.models[ this.model_names[0] ].all();
} else {
return this.models;
}
},
/*
Add another sheet to the wanted list
*/
addWanted: function(sheet) {
if(ttIndexOf(this.wanted, sheet) === -1) {
this.wanted.push(sheet);
}
},
/*
Load all worksheets of the spreadsheet, turning each into a Tabletop Model.
Need to use injectScript because the worksheet view that you're working from
doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though.
Calls back to loadSheet in order to get the real work done.
Used as a callback for the worksheet-based JSON
*/
loadSheets: function(data) {
var i, ilen;
var toLoad = [];
this.foundSheetNames = [];
for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) {
this.foundSheetNames.push(data.feed.entry[i].title.$t);
// Only pull in desired sheets to reduce loading
if( this.isWanted(data.feed.entry[i].content.$t) ) {
var linkIdx = data.feed.entry[i].link.length-1;
var sheet_id = data.feed.entry[i].link[linkIdx].href.split('/').pop();
var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?alt="
if (inNodeJS || supportsCORS) {
json_path += 'json';
} else {
json_path += 'json-in-script';
}
if(this.query) {
json_path += "&sq=" + this.query;
}
if(this.orderby) {
json_path += "&orderby=column:" + this.orderby.toLowerCase();
}
if(this.reverse) {
json_path += "&reverse=true";
}
toLoad.push(json_path);
}
}
this.sheetsToLoad = toLoad.length;
for(i = 0, ilen = toLoad.length; i < ilen; i++) {
this.requestData(toLoad[i], this.loadSheet);
}
},
/*
Access layer for the this.models
.sheets() gets you all of the sheets
.sheets('Sheet1') gets you the sheet named Sheet1
*/
sheets: function(sheetName) {
if(typeof sheetName === "undefined") {
return this.models;
} else {
if(typeof(this.models[ sheetName ]) === "undefined") {
// alert( "Can't find " + sheetName );
return;
} else {
return this.models[ sheetName ];
}
}
},
/*
Parse a single list-based worksheet, turning it into a Tabletop Model
Used as a callback for the list-based JSON
*/
loadSheet: function(data) {
var model = new Tabletop.Model( { data: data,
parseNumbers: this.parseNumbers,
postProcess: this.postProcess,
tabletop: this } );
this.models[ model.name ] = model;
if(ttIndexOf(this.model_names, model.name) === -1) {
this.model_names.push(model.name);
}
this.sheetsToLoad--;
if(this.sheetsToLoad === 0)
this.doCallback();
},
/*
Execute the callback upon loading! Rely on this.data() because you might
only request certain pieces of data (i.e. simpleSheet mode)
Tests this.sheetsToLoad just in case a race condition happens to show up
*/
doCallback: function() {
if(this.sheetsToLoad === 0) {
this.callback.apply(this.callbackContext || this, [this.data(), this]);
}
},
log: function(msg) {
if(this.debug) {
if(typeof console !== "undefined" && typeof console.log !== "undefined") {
Function.prototype.apply.apply(console.log, [console, arguments]);
}
}
}
};
/*
Tabletop.Model stores the attribute names and parses the worksheet data
to turn it into something worthwhile
Options should be in the format { data: XXX }, with XXX being the list-based worksheet
*/
Tabletop.Model = function(options) {
var i, j, ilen, jlen;
this.column_names = [];
this.name = options.data.feed.title.$t;
this.elements = [];
this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae
if(typeof(options.data.feed.entry) === 'undefined') {
options.tabletop.log("Missing data for " + this.name + ", make sure you didn't forget column headers");
this.elements = [];
return;
}
for(var key in options.data.feed.entry[0]){
if(/^gsx/.test(key))
this.column_names.push( key.replace("gsx$","") );
}
for(i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) {
var source = options.data.feed.entry[i];
var element = {};
for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) {
var cell = source[ "gsx$" + this.column_names[j] ];
if (typeof(cell) !== 'undefined') {
if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t))
element[ this.column_names[j] ] = +cell.$t;
else
element[ this.column_names[j] ] = cell.$t;
} else {
element[ this.column_names[j] ] = '';
}
}
if(element.rowNumber === undefined)
element.rowNumber = i + 1;
if( options.postProcess )
options.postProcess(element);
this.elements.push(element);
}
};
Tabletop.Model.prototype = {
/*
Returns all of the elements (rows) of the worksheet as objects
*/
all: function() {
return this.elements;
},
/*
Return the elements as an array of arrays, instead of an array of objects
*/
toArray: function() {
var array = [],
i, j, ilen, jlen;
for(i = 0, ilen = this.elements.length; i < ilen; i++) {
var row = [];
for(j = 0, jlen = this.column_names.length; j < jlen ; j++) {
row.push( this.elements[i][ this.column_names[j] ] );
}
array.push(row);
}
return array;
}
};
if(inNodeJS) {
module.exports = Tabletop;
} else {
global.Tabletop = Tabletop;
}
})(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment