Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@micahstubbs
Last active July 19, 2016 21:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save micahstubbs/948378950dd7b3e31e8cda3b33ebbcc8 to your computer and use it in GitHub Desktop.
Save micahstubbs/948378950dd7b3e31e8cda3b33ebbcc8 to your computer and use it in GitHub Desktop.
Blocks Graph IV - d3.oakland talk
license: CC0-1.0
height: 1000
border: no

an updated look at README.md files from bl.ocks.org that contain links to other bl.ocks. These data gathered in early May 2016.

click on a node to view that bl.ock

community detection done with the jLouvain library

source data and scripts used to generate the graph data are at this repository

a lineage of bl.ocks that informed this idea:

<html>
<head>
<title>Blocks Graph - Readme Mentions</title>
<meta charset='utf-8' />
<script src='https://d3js.org/d3.v3.min.js'></script>
<script src='https://npmcdn.com/babel-core@5.8.34/browser.min.js'></script>
<script src='jsLouvain.js'></script>
</head>
<body>
<script lang='babel' type='text/babel'>
/* Make the vis fill the page using CSS. */
d3.select('body')
.style({
margin: 0,
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0
});
d3.select('body').append('div')
.attr('id', 'viz')
.style({
float: 'left',
width: '70%',
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0
})
d3.select('body').append('div')
.attr('id', 'detail')
.style({
float: 'right',
width: '30%',
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: '70%'
})
// Extract the width and height that was computed by CSS.
const vizDiv = document.getElementById('viz');
const width = vizDiv.clientWidth;
const height = vizDiv.clientHeight;
const minSide = d3.min([width, height]);
const xOffset = (width - minSide) / 2;
const yOffset = (height - minSide) / 2;
const detailDiv = document.getElementById('detail');
const detailHeight = detailDiv.clientHeight;
const detailWidth = detailDiv.clientWidth;
const detailSVG = d3.select(detailDiv)
.append('svg')
.attr('width', detailDiv.clientWidth)
.attr('height', detailDiv.clientHeight);
let cardsOnPage = [];
let cardsDisplayed = [];
const nodeHash = {};
// Use the extracted size to set the size of a Canvas element
d3.select(vizDiv).append('canvas')
.attr('width', width)
.attr('height', height)
.style('position', 'absolute');
// Use the extracted size to set the size of a SVG element
d3.select(vizDiv).append('svg')
.attr('width', width)
.attr('height', height)
.classed('main', true)
.style('position', 'absolute');
d3.json('readme-blocks-graph.json', function (error, data) {
createNetwork(error, data);
});
// remove thumbnails url data loading for now
// queue()
// .defer(d3.json, 'readme-blocks-graph.json')
// .defer(d3.json, 'thumbnailsHash.json')
// .await((error, data1, data2) => createNetwork(error, data1, data2));
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
function createNetwork(error, graphContainer) {
console.log('graphContainer', graphContainer);
const nodes = [];
const edges = [];
const edgelist = graphContainer.graph.edges;
const nodelist = graphContainer.graph.nodes;
edgelist.forEach(edge => {
if (!nodeHash[edge.source]) {
nodeHash[edge.source] = {
id: edge.source,
label: edge.source
};
nodes.push(nodeHash[edge.source]);
}
if (!nodeHash[edge.target]) {
nodeHash[edge.target] = {
id: edge.target,
label: edge.target
};
nodes.push(nodeHash[edge.target]);
}
if (edge /* .weight == 5 */) {
edges.push({
id: `${nodeHash[edge.source].id}-${nodeHash[edge.target].id}`,
source: nodeHash[edge.source],
target: nodeHash[edge.target],
weight: 1 /* edge.weight */
});
}
});
// get some attributes from the nodelist (calculated elsewhere)
// and store them in the nodeHash
nodelist.forEach(node => {
if (nodeHash[node.id]) {
nodeHash[node.id].user = node.user;
nodeHash[node.id].createdAt = node.createdAt;
nodeHash[node.id].updatedAt = node.updatedAt;
nodeHash[node.id].description = node.description;
}
});
// take the attributes now in the nodeHash
// and hang them on the nodes (calculated here from the edgelist)
nodes.forEach(node => {
if (nodeHash[node.id]) {
node.user = nodeHash[node.id].user;
node.createdAt = nodeHash[node.id].createdAt;
node.updatedAt = nodeHash[node.id].updatedAt;
node.description = nodeHash[node.id].description;
}
});
console.log('nodes', nodes);
console.log('edges', edges);
console.log('nodeHash', nodeHash);
createForceNetwork(nodes, edges);
}
function modularityCensus(nodes, edges /* , moduleHash */) {
edges.forEach(edge => {
if (edge.source.module !== edge.target.module) {
edge.border = true;
} else {
edge.border = false;
}
});
nodes.forEach(node => {
const theseEdges = edges.filter(d => d.source === node || d.target === node);
const theseSourceModules = theseEdges.map(d => d.source.module).filter(onlyUnique);
const theseTargetModules = theseEdges.map(d => d.target.module).filter(onlyUnique);
if (theseSourceModules.length > 1 || theseTargetModules.length > 1) {
node.border = true;
} else {
node.border = false;
}
});
}
function createForceNetwork(nodes, edges) {
// create a network from an edgelist
// var colors = d3.scale.ordinal().domain([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]).range(['#996666', '#66CCCC', '#FFFF99', '#CC9999', '#666633', '#993300', '#999966', '#660000', '#996699', '#cc6633', '#ff9966', '#339999', '#6699cc', '#ffcc66', '#ff6600', '#00ccccc']);
const colors = d3.scale.category20();
const nodeData = nodes.map(d => d.id);
const edgeData = edges.map(function (d) {
return {
source: d.source.id,
target: d.target.id,
weight: 1
};
});
console.log('nodeData for jLouvain', nodeData, 'nodes', nodeData.length);
console.log('edgeData for jLouvain', edgeData, 'edges', edgeData.length);
const community = jLouvain().nodes(nodeData).edges(edgeData);
const result = community();
console.log('jLouvain result', result);
nodes.forEach(node => {
node.module = result[node.id];
// console.log('node.module', node.module)
});
modularityCensus(nodes, edges, result);
const force = d3.layout.force().nodes(nodes).links(edges)
.size([minSide, minSide]) // make a square // minSide, minSide
.charge(-100)
.chargeDistance(100)
.linkStrength(2)
.linkDistance(1)
.gravity(0.07);
const nodeEnter = d3.select('svg.main').selectAll('g.node')
.data(nodes, d => d.id)
.enter();
nodeEnter
.append('a')
.attr('xlink:href', d => `http://bl.ocks.org/${d.user}/${d.id}`)
.attr('target', '_blank')
.attr('id', d => d.id)
// .attr('target', '_blank')
.append('circle')
.attr('r', 3)
.attr('class', 'foreground')
.style('fill', d => colors(d.module))
.style('stroke-width', d => {
if (d.border) return '3px';
return '1px';
})
.on('mouseover', d => nodeMouseover(d))
.on('mouseout', () => nodeMouseout());
/*
// draw labels over nodes
nodeEnter.append('text')
.style('text-anchor', 'middle')
.attr('y', 3)
.style('stroke-width', '1px')
.style('stroke-opacity', 0.75)
.style('stroke', 'white')
.style('font-size', '8px')
.text(function (d) {return d.id})
.style('pointer-events', 'none')
nodeEnter.append('text')
.style('text-anchor', 'middle')
.attr('y', 3)
.style('font-size', '8px')
.text(function (d) {return d.id})
.style('pointer-events', 'none')
*/
force.start();
for (let i = 0; i < 200; i++) {
force.tick();
}
// draw the network at each tick
// force.on('tick', updateNetwork);
// after 200 iterations, we say the network
// has stabilized and we stop the
// force layout physics simulation
force.stop();
// now we draw the network
updateNetwork(edges);
// drawVoronoiOverlay();
function updateNetwork(edgesToUpdate) {
// // draw the links
// d3.select('svg.main').selectAll('line')
// .attr('x1', d => d.source.x)
// .attr('y1', d => d.source.y)
// .attr('x2', d => d.target.x)
// .attr('y2', d => d.target.y)
// .attr('transform', 'translate(' + xOffset + ',' + yOffset +')');
const context = d3.select('canvas').node().getContext('2d');
context.clearRect(0, 0, width, height);
context.translate(xOffset, yOffset);
// draw links (or edges, if you prefer)
context.strokeStyle = 'rgba(0, 0, 0, 0.5)';
context.beginPath();
edgesToUpdate.forEach(d => {
context.moveTo(d.source.x, d.source.y);
context.lineTo(d.target.x, d.target.y);
});
context.stroke();
// draw the nodes
d3.select('svg.main').selectAll('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('transform', `translate(${xOffset}, ${yOffset})`);
}
function drawVoronoiOverlay() {
const rawPoints = [];
nodes.forEach(d => {
rawPoints.push({
x: d.x,
y: d.y,
id: d.id,
user: d.user,
description: d.description
});
});
const voronoi = d3.geom.voronoi();
voronoi
.x(d => d.x)
.y(d => d.y)
.clipExtent([[-10 - xOffset, -10 - yOffset], [width + 10, height + 10]]);
voronoiData = voronoi(rawPoints);
voronoiData = voronoiData.map(function (d, i) {
return {
coordinates: d,
data: rawPoints[i]
};
});
console.log('voronoiData', voronoiData);
const voronoiPaths = d3.select('svg.main').selectAll('path.voronoi')
.data(voronoiData)
.enter()
.insert('path')
.attr('class', 'voronoi')
.style('stroke', 'none')
.style('stroke-width', '1px')
.style('fill', 'white')
.style('fill-opacity', 0)
.attr('d', d => `M${d.coordinates.join('L')}Z`)
.attr('transform', `translate(${xOffset}, ${yOffset})`);
voronoiPaths
.on('mouseover', d => nodeMouseover(d))
.on('click', d => nodeClick(d))
.on('mouseout', () => nodeMouseout());
}
function nodeMouseover(d) {
console.log('nodeHash', nodeHash);
// allow nodeMousever to be called in different contexts
// either from the node circle or from the Voronoi path
let id;
let user;
let description;
const dH = 30;
if (typeof d.data === 'undefined') {
id = d.id;
user = nodeHash[d.id].user;
description = nodeHash[d.id].description;
}
else {
id = d.data.id;
user = d.data.user;
d.data.description;
}
const blockUrl = `http://bl.ocks.org/${user}/${id}`
// generate nice text to display on the small canvas below the thumbnail image
let blockTitleText;
if (
['undefined', 'null'].indexOf(String(user)) === -1 &&
['undefined', 'null'].indexOf(String(description)) === -1
) {
blockTitleText = `${user} | ${description}`
}
if (
['undefined', 'null'].indexOf(String(user)) === -1 &&
['undefined', 'null'].indexOf(String(description)) > -1
) {
blockTitleText = user;
}
if (
['undefined', 'null'].indexOf(String(user)) > -1 &&
['undefined', 'null'].indexOf(String(description)) === -1
) {
blockTitleText = description;
}
if (
['undefined', 'null'].indexOf(String(user)) > -1 &&
['undefined', 'null'].indexOf(String(description)) > -1
) {
blockTitleText = '';
}
console.log(blockUrl);
console.log(blockTitleText);
// const cardDiv = d3.select(detailDiv).append('div')
// .attr('id', `card${id}`)
// .append('a')
// .attr('xlink:href', d => `http://bl.ocks.org/${user}/${id}`)
// .innerHTML(blockTitleText);
//if (cardsDisplayed.length > 10) {
// d3.selectAll('.cards')
// .attr('transform', `translate(0,${15 * (cardsOnPage.length-1)})`);
//}
// draw a rectangle
const detailG = detailSVG.append('a')
.attr('xlink:href', blockUrl)
.attr('target', '_blank')
.attr('id', `card${id}`)
.classed('cards', true)
.append('rect')
//.classed('cards', true)
.attr('id', `card${id}`)
.attr('x', 0)
.attr('y', 5 + dH * cardsOnPage.length)
.attr('height', dH)
.attr('width', detailDiv.clientWidth)
.style('stroke-width', 1)
.style('stroke', 'white')
.style('fill', 'lightgray')
.style('fill-opacity', .4)
.attr('rx', 4)
.attr('ry', 4);
// draw text on the screen
detailSVG.append('text')
.attr('x', 10)
.attr('y', 5 + dH * cardsOnPage.length)
.classed('cards', true)
.attr('id', `card${id}`)
.style('fill', 'black')
.style('font-size', '16px')
.attr('dy', '20px')
.attr('text-anchor', 'start')
.style('pointer-events', 'none')
.text(blockTitleText);
console.log('cardsOnPage', cardsOnPage);
console.log('cardsOnPage.length', cardsOnPage.length);
if (false/*cardsDisplayed.length > 10*/) {
const lastCardId = cardsOnPage[cardsOnPage.length - 1];
const firstCardId = cardsOnPage[0];
const cardSelector = `div#card${firstCardId}`.replace(/[,.]/g, '');
d3.select(cardSelector)
.remove();
// d3.selectAll('.cards')
// .attr('transform', `translate(0,-15)`);
//cardsOnPage.pop();
cardsOnPage.shift();
//if(cardsOnPage.indexOf(id) > -1) cardsOnPage = removeByValue(cardsOnPage, id);
}
cardsOnPage.push(id);
cardsDisplayed.push(id);
d3.select(vizDiv).select('svg').append('text')
.attr('x', 100)
.attr('y', 100)
.classed('titleText', true)
.style('fill', 'black')
.style('fill-opacity', 0.8)
.style('font-size', '60px')
.style('font-weight', 600)
.attr('dy', '0px')
.attr('text-anchor', 'start')
.style('pointer-events', 'none')
.text(blockTitleText);
}
function nodeMouseout() {
d3.selectAll('.titleText')
.remove();
}
// http://stackoverflow.com/a/11223909/1732222
function ImageExist(url)
{
var img = new Image();
img.src = url;
return img.height != 0;
}
// http://stackoverflow.com/a/3955096/1732222
function removeByValue(arr) {
var what, a = arguments, L = a.length, ax;
while (L > 1 && arr.length) {
what = a[--L];
while ((ax= arr.indexOf(what)) !== -1) {
arr.splice(ax, 1);
}
}
return arr;
}
function nodeClick(d) {
const user = d.data.user;
const id = d.data.id;
const blockUrl = `http://bl.ocks.org/${user}/${id}`
openInNewTab(blockUrl);
}
// http://stackoverflow.com/a/11384018/1732222
function openInNewTab(url) {
const win = window.open(url, '_blank');
win.focus();
}
}
</script>
</body>
</html>
/*
Author: Corneliu S. (github.com/upphiminn)
This is a javascript implementation of the Louvain
community detection algorithm (http://arxiv.org/abs/0803.0476)
Based on https://bitbucket.org/taynaud/python-louvain/overview
*/
(function(){
jLouvain = function(){
//Constants
var __PASS_MAX = -1
var __MIN = 0.0000001
//Local vars
var original_graph_nodes;
var original_graph_edges;
var original_graph = {};
var partition_init;
//Helpers
function make_set(array){
var set = {};
array.forEach(function(d,i){
set[d] = true;
});
return Object.keys(set);
};
function obj_values(obj){
var vals = [];
for( var key in obj ) {
if ( obj.hasOwnProperty(key) ) {
vals.push(obj[key]);
}
}
return vals;
};
function get_degree_for_node(graph, node){
var neighbours = graph._assoc_mat[node] ? Object.keys(graph._assoc_mat[node]) : [];
var weight = 0;
neighbours.forEach(function(neighbour,i){
var value = graph._assoc_mat[node][neighbour] || 1;
if(node == neighbour)
value *= 2;
weight += value;
});
return weight;
};
function get_neighbours_of_node(graph, node){
if(typeof graph._assoc_mat[node] == 'undefined')
return [];
var neighbours = Object.keys(graph._assoc_mat[node]);
return neighbours;
}
function get_edge_weight(graph, node1, node2){
return graph._assoc_mat[node1] ? graph._assoc_mat[node1][node2] : undefined;
}
function get_graph_size(graph){
var size = 0;
graph.edges.forEach(function(edge){
size += edge.weight;
});
return size;
}
function add_edge_to_graph(graph, edge){
update_assoc_mat(graph, edge);
var edge_index = graph.edges.map(function(d){
return d.source+'_'+d.target;
}).indexOf(edge.source+'_'+edge.target);
if(edge_index != -1)
graph.edges[edge_index].weight = edge.weight;
else
graph.edges.push(edge);
}
function make_assoc_mat(edge_list){
var mat = {};
edge_list.forEach(function(edge, i){
mat[edge.source] = mat[edge.source] || {};
mat[edge.source][edge.target] = edge.weight;
mat[edge.target] = mat[edge.target] || {};
mat[edge.target][edge.source] = edge.weight;
});
return mat;
}
function update_assoc_mat(graph, edge){
graph._assoc_mat[edge.source] = graph._assoc_mat[edge.source] || {};
graph._assoc_mat[edge.source][edge.target] = edge.weight;
graph._assoc_mat[edge.target] = graph._assoc_mat[edge.target] || {};
graph._assoc_mat[edge.target][edge.source] = edge.weight;
}
function clone(obj){
if(obj == null || typeof(obj) != 'object')
return obj;
var temp = obj.constructor();
for(var key in obj)
temp[key] = clone(obj[key]);
return temp;
}
//Core-Algorithm Related
function init_status(graph, status, part){
status['nodes_to_com'] = {};
status['total_weight'] = 0;
status['internals'] = {};
status['degrees'] = {};
status['gdegrees'] = {};
status['loops'] = {};
status['total_weight'] = get_graph_size(graph);
if(typeof part == 'undefined'){
graph.nodes.forEach(function(node,i){
status.nodes_to_com[node] = i;
var deg = get_degree_for_node(graph, node);
if (deg < 0)
throw 'Bad graph type, use positive weights!';
status.degrees[i] = deg;
status.gdegrees[node] = deg;
status.loops[node] = get_edge_weight(graph, node, node) || 0;
status.internals[i] = status.loops[node];
});
}else{
graph.nodes.forEach(function(node,i){
var com = part[node];
status.nodes_to_com[node] = com;
var deg = get_degree_for_node(graph, node);
status.degrees[com] = (status.degrees[com] || 0) + deg;
status.gdegrees[node] = deg;
var inc = 0.0;
var neighbours = get_neighbours_of_node(graph, node);
neighbours.forEach(function(neighbour, i){
var weight = graph._assoc_mat[node][neighbour];
if (weight <= 0){
throw "Bad graph type, use positive weights";
}
if(part[neighbour] == com){
if (neighbour == node){
inc += weight;
}else{
inc += weight/2.0;
}
}
});
status.internals[com] = (status.internals[com] || 0) + inc;
});
}
}
function __modularity(status){
var links = status.total_weight;
var result = 0.0;
var communities = make_set(obj_values(status.nodes_to_com));
communities.forEach(function(com,i){
var in_degree = status.internals[com] || 0 ;
var degree = status.degrees[com] || 0 ;
if(links > 0){
result = result + in_degree / links - Math.pow((degree / (2.0*links)), 2);
}
});
return result;
}
function __neighcom(node, graph, status){
// compute the communities in the neighb. of the node, with the graph given by
// node_to_com
var weights = {};
var neighboorhood = get_neighbours_of_node(graph, node);//make iterable;
neighboorhood.forEach(function(neighbour, i){
if(neighbour != node){
var weight = graph._assoc_mat[node][neighbour] || 1;
var neighbourcom = status.nodes_to_com[neighbour];
weights[neighbourcom] = (weights[neighbourcom] || 0) + weight;
}
});
return weights;
}
function __insert(node, com, weight, status){
//insert node into com and modify status
status.nodes_to_com[node] = +com;
status.degrees[com] = (status.degrees[com] || 0) + (status.gdegrees[node]||0);
status.internals[com] = (status.internals[com] || 0) + weight + (status.loops[node]||0);
}
function __remove(node, com, weight, status){
//remove node from com and modify status
status.degrees[com] = ((status.degrees[com] || 0) - (status.gdegrees[node] || 0));
status.internals[com] = ((status.internals[com] || 0) - weight -(status.loops[node] ||0));
status.nodes_to_com[node] = -1;
}
function __renumber(dict){
var count = 0;
var ret = clone(dict); //deep copy :)
var new_values = {};
var dict_keys = Object.keys(dict);
dict_keys.forEach(function(key){
var value = dict[key];
var new_value = typeof new_values[value] =='undefined' ? -1 : new_values[value];
if(new_value == -1){
new_values[value] = count;
new_value = count;
count = count + 1;
}
ret[key] = new_value;
});
return ret;
}
function __one_level(graph, status){
//Compute one level of the Communities Dendogram.
var modif = true,
nb_pass_done = 0,
cur_mod = __modularity(status),
new_mod = cur_mod;
while (modif && nb_pass_done != __PASS_MAX){
cur_mod = new_mod;
modif = false;
nb_pass_done += 1
graph.nodes.forEach(function(node,i){
var com_node = status.nodes_to_com[node];
var degc_totw = (status.gdegrees[node] || 0) / (status.total_weight * 2.0);
var neigh_communities = __neighcom(node, graph, status);
__remove(node, com_node, (neigh_communities[com_node] || 0.0), status);
var best_com = com_node;
var best_increase = 0;
var neigh_communities_entries = Object.keys(neigh_communities);//make iterable;
neigh_communities_entries.forEach(function(com,i){
var incr = neigh_communities[com] - (status.degrees[com] || 0.0) * degc_totw;
if (incr > best_increase){
best_increase = incr;
best_com = com;
}
});
__insert(node, best_com, neigh_communities[best_com] || 0, status);
if(best_com != com_node)
modif = true;
});
new_mod = __modularity(status);
if(new_mod - cur_mod < __MIN)
break;
}
}
function induced_graph(partition, graph){
var ret = {nodes:[], edges:[], _assoc_mat: {}};
var w_prec, weight;
//add nodes from partition values
var partition_values = obj_values(partition);
ret.nodes = ret.nodes.concat(make_set(partition_values)); //make set
graph.edges.forEach(function(edge,i){
weight = edge.weight || 1;
var com1 = partition[edge.source];
var com2 = partition[edge.target];
w_prec = (get_edge_weight(ret, com1, com2) || 0);
var new_weight = (w_prec + weight);
add_edge_to_graph(ret, {'source': com1, 'target': com2, 'weight': new_weight});
});
return ret;
}
function partition_at_level(dendogram, level){
var partition = clone(dendogram[0]);
for(var i = 1; i < level + 1; i++ )
Object.keys(partition).forEach(function(key,j){
var node = key;
var com = partition[key];
partition[node] = dendogram[i][com];
});
return partition;
}
function generate_dendogram(graph, part_init){
if(graph.edges.length == 0){
var part = {};
graph.nodes.forEach(function(node,i){
part[node] = node;
});
return part;
}
var status = {};
init_status(original_graph, status, part_init);
var mod = __modularity(status);
var status_list = [];
__one_level(original_graph, status);
var new_mod = __modularity(status);
var partition = __renumber(status.nodes_to_com);
status_list.push(partition);
mod = new_mod;
var current_graph = induced_graph(partition, original_graph);
init_status(current_graph, status);
while (true){
__one_level(current_graph, status);
new_mod = __modularity(status);
if(new_mod - mod < __MIN)
break;
partition = __renumber(status.nodes_to_com);
status_list.push(partition);
mod = new_mod;
current_graph = induced_graph(partition, current_graph);
init_status(current_graph, status);
}
return status_list;
}
var core = function(){
var status = {};
var dendogram = generate_dendogram(original_graph, partition_init);
return partition_at_level(dendogram, dendogram.length - 1);
};
core.nodes = function(nds){
if(arguments.length > 0){
original_graph_nodes = nds;
}
return core;
};
core.edges = function(edgs){
if(typeof original_graph_nodes == 'undefined')
throw 'Please provide the graph nodes first!';
if(arguments.length > 0){
original_graph_edges = edgs;
var assoc_mat = make_assoc_mat(edgs);
original_graph = { 'nodes': original_graph_nodes,
'edges': original_graph_edges,
'_assoc_mat': assoc_mat };
}
return core;
};
core.partition_init = function(prttn){
if(arguments.length > 0){
partition_init = prttn;
}
return core;
};
return core;
}
})();
View raw

(Sorry about that, but we can’t show files that are this big right now.)

This file has been truncated, but you can view the full file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment