Built with blockbuilder.org
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.4.0/d3-legend.min.js"></script>
<style>
body {
margin:0;
position:absolute;
top:0;right:0;bottom:0;left:0;
overflow-y: scroll;
}
svg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
text {
font-family: 'Helvetica';
fill: #666;
}
.brush .extent {
fill: #555;
fill-opacity: .1;
stroke: #fff;
shape-rendering: crispEdges;
}
.hide {
display: none
}
</style>
</head>
<body>
<script>
var margin = {top: 20, right: 10, bottom: 20, left: 10};
var width = 1050 - margin.left - margin.right;
var height = 1500 - margin.top - margin.bottom;
var duration = 100;
var radius = 250;
var apiSize = 50;
var ignoreApi = ['d3.select', 'd3.selectAll'];
var selected = {};
var hovered = false;
var brushing = false;
var zoom = d3.behavior.zoom()
.size([window.innerWidth, window.innerHeight])
.on('zoom', panSummary);
var force = d3.layout.force()
.size([850, height])
.charge(function(d) {return d.type === 'api' ? -10 : -Math.pow(d.size, 2)})
.linkStrength(function(d) {return d.strength})
.on('tick', updateGraph);
var blocks = {};
var api = {};
var apiNodes = [];
var blockNodes = [];
var nodes = [],
links = [];
var sizeScale;
var widthScale;
var strengthScale;
var colors;
var totalApiSize = 0;
var circle, path, text, summary;
var svg = d3.select("body").append("svg").append("g");
var summaries = svg.append('g')
.attr('transform', 'translate(' + width + ',0)');
var graph = svg.append('g');
graph.append('rect')
.attr('width', width)
.attr('height', height + margin.top + margin.bottom)
.attr('fill', '#fff')
.attr('opacity', .9);
var svgBrush = d3.svg.brush()
.x(d3.scale.identity().domain([(width / 2) - radius, (width / 2) + radius]))
.y(d3.scale.identity().domain([(height / 2) - radius, (height / 2) + radius]))
.on('brushstart', function() {
summaries.attr('transform', 'translate(' + (width + margin.left + margin.right) + ',0)');
brushing = true;
}).on("brush", brush)
.on('brushend', function() {brushing = false;});
graph.append("g")
.attr("class", "brush")
.call(svgBrush);
function enterGraph() {
path = graph.selectAll('line')
.data(links).enter().insert('line', '.brush')
.attr('stroke', '#ccc')
.attr('stroke-linecap', 'round');
circle = graph.selectAll('circle')
.data(nodes).enter().append('circle')
.classed('node', true)
.attr('stroke', 'none')
.attr('stroke-width', 3)
.style({'cursor': 'pointer'})
.on('mouseover', hover)
.on('mouseleave', unhover)
.on('click', select);
text = graph.selectAll('text')
.data(_.filter(nodes, function(node) {return node.type === 'api';}))
.enter().append('text')
.classed('nodeText', true)
.attr('dy', '.35em')
.style({
'font-size': '12px',
'font-weight': 600,
'cursor': 'pointer'
}).on('mouseover', hover)
.on('mouseleave', unhover)
.on('click', select);
};
function updateGraph() {
circle.attr('r', function(d) {return d.size})
.attr('fill', function(d) {return d.fill || '#fff'})
.attr('cx', function(d) {return d.x})
.attr('cy', function(d) {return d.y});
text
.attr('text-anchor', function(d, i) {
return i < (apiSize / 2) ? 'start' : 'end';
}).attr('transform', function(d, i) {
var angle;
if (i < (apiSize / 2)) {
angle = (360 / apiSize) * i - 90;
} else {
angle = -(360 / apiSize) * (apiSize - i) + 90;
}
return 'translate(' + d.x + ',' + d.y +
')rotate(' + angle + ')';
}).text(function(d) {return d.id});
path.attr('stroke-width', function(d) {return d.size})
.attr('x1', function(d) {return d.source.x})
.attr('y1', function(d) {return d.source.y})
.attr('x2', function(d) {return d.target.x})
.attr('y2', function(d) {return d.target.y});
applyHighlight();
};
function renderSummary() {
var selectedData = _.chain(selected)
.filter(function(d) {return d.type !== 'api'})
.sortBy(function(d) {return d.image})
.value();
if (_.isEmpty(selectedData)) {
summaries.classed('hide', true)
.attr('transform', 'translate(' + (width + margin.left + margin.right) + ',0)');
} else {
summaries.classed('hide', false);
}
summary = summaries.selectAll('g').data(selectedData);
var enterSummary = summary.enter().append('g')
.classed('summary', true);
enterSummary.append('rect');
enterSummary.append('text')
.classed('title', true);
enterSummary.append('image');
enterSummary.append('text')
.classed('username', true);
var padding = 7.5;
var textHeight = 12;
var imageWidth = 230;
var imageHeight = 120;
var summaryWidth = 300;
var summaryHeight = (imageHeight + 8 * padding + textHeight);
summary.select('rect')
.attr('width', imageWidth + 2 * padding)
.attr('height', imageHeight + 4 * padding + 2 * textHeight)
.attr('rx', 3).attr('ry', 3)
.attr('fill', '#fcfcfc')
.attr('stroke-width', 3);
summary.select('.title')
.attr('dy', '.35em')
.attr('transform', 'translate(' + padding + ',' + (padding + textHeight / 2) + ')')
.style({
'font-weight': 600
}).text(function(d) {
var title = d.title;
if (title && title.length > 25) {
title = title.substring(0, 25) + ' ...';
}
return title;
});
summary.select('image')
.attr('xlink:href', function(d) {
return d.image || 'visfest4.png';
}).attr('width', imageWidth)
.attr('height', imageHeight)
.attr('transform', 'translate(' + padding + ',' + (textHeight + 2 * padding) + ')');
summary.select('.username')
.attr('dy', '.35em')
.attr('transform', 'translate(' + padding + ',' +
(4 * padding + textHeight + imageHeight) + ')')
.style({
'font-size': textHeight
}).text(function(d) {return d.user});
var heightSize = Math.floor(2 * radius / (imageHeight + 4 * padding));
summary.attr('transform', function(d, i) {
d.summaryX = summaryWidth * (Math.floor(i / heightSize));
d.summaryY = summaryHeight * (i % heightSize) + ((height / 2) - radius);
return 'translate(' + d.summaryX + ',' + d.summaryY + ')';
}).style({
'cursor': 'pointer'
}).on('click', function(d) {
window.open('//bl.ocks.org/' + d.user + '/' + d.id, '_blank');
}).on('mouseover', hover)
.on('mouseleave', unhover);
summary.exit().remove();
};
function hover() {
if (brushing) return;
hovered = d3.select(this).datum();
calculateHighlight(hovered, true, 'hovered');
applyHighlight(hovered);
renderSummary();
}
function unhover() {
if (brushing) return;
hovered = false;
calculateHighlight(d3.select(this).datum(), false, 'hovered');
applyHighlight();
renderSummary();
}
function select() {
if (brushing) return;
var data = d3.select(this).datum();
if (selected[data.id]) {
delete selected[data.id];
calculateHighlight(data, false, 'selected');
} else {
selected[data.id] = data;
calculateHighlight(data, true, 'selected');
}
renderSummary();
}
function brush() {
selected = {};
var extent = d3.event.target.extent();
circle.each(function(d) {
if (d.type !== 'api' &&
extent[0][0] <= d.x && d.x < extent[1][0]
&& extent[0][1] <= d.y && d.y < extent[1][1]) {
selected[d.id] = d;
}
});
calculateHighlight(null, true, 'selected');
applyHighlight();
renderSummary();
}
function panSummary() {
svg.attr('transform', 'translate(' + d3.event.translate + ')');
}
function searchUsername(e) {
var chara = d3.event.keyCode;
var value = d3.event.target.value;
if (chara === 13) {
selected = {};
svgBrush.empty();
var block = _.chain(blockNodes)
.filter(function(block) {
return value && block.user.match(value);
}).each(function(block) {
selected[block.id] = block;
}).value();
calculateHighlight(null, true, 'selected');
applyHighlight();
renderSummary();
} else if (chara === 27) {
selected = {};
calculateHighlight(null, true, 'selected');
applyHighlight();
renderSummary();
value = d3.event.target.value = null;
d3.event.target.blur();
}
var matched = _.filter(blockNodes, function(block) {
return value && block.user.match(value);
});
var matchedText = value ? (matched.length + ' matches') : '';
numMatches.text(matchedText);
}
function calculateHighlight(data, highlight, type) {
var nodesToHighlight = {};
path.each(function(d) {
d.selected = false;
d.hovered = false;
if (selected[d.source.id]) {
// if it's highlighted because it's been previously selected
if (nodesToHighlight[d.target.id] !== 'hovered') {
nodesToHighlight[d.target.id] = 'selected';
}
d.selected = true;
} else if (selected[d.target.id]) {
if (nodesToHighlight[d.source.id] !== 'hovered') {
nodesToHighlight[d.source.id] = 'selected';
}
d.selected = true;
}
if (highlight && data && d.source.id === data.id) {
// if path is the one being hovered or selected
if (nodesToHighlight[d.target.id] !== 'hovered') {
nodesToHighlight[d.target.id] = type;
}
d[type] = true;
} else if (highlight && data && d.target.id === data.id) {
if (nodesToHighlight[d.source.id] !== 'hovered') {
nodesToHighlight[d.source.id] = type;
}
d[type] = true;
}
});
d3.selectAll('.node,.nodeText,.summary').each(function(d) {
d.selected = false;
d.hovered = false;
if (selected[d.id]) {
// if previously selected
d.selected = true;
}
if (nodesToHighlight[d.id]) {
// if on other side of a node being hovered or selected
d[nodesToHighlight[d.id]] = true;
}
if (highlight && data && d.id === data.id) {
// if circle is being hovered or selected
d[type] = true;
}
});
}
function applyHighlight(hoveredData) {
path.attr('stroke', function(d) {
return d.hovered ? '#FFD84B' : '#ccc';
}).transition().duration(brushing ? 0 : duration)
.attr('stroke-opacity', function(d) {
if (d.selected || d.hovered) {
return 1;
} else if (_.isEmpty(selected) && !hovered) {
return .5;
} else {
return .15;
}
});
circle.attr('stroke', function(d) {
return d.hovered ? '#FFD84B' : 'none';
}).transition().duration(brushing ? 0 : duration)
.attr('fill-opacity', function(d) {
if (d.selected || d.hovered) {
return d.type === 'api' ? .5 : 1;
} else if (_.isEmpty(selected) && !hovered) {
return d.type === 'api' ? .25 : 1;
} else {
return .15;
}
});
text.transition().duration(brushing ? 0 : duration)
.attr('fill-opacity', function(d) {
return d.selected || d.hovered ||
(_.isEmpty(selected) && !hovered) ? 1 : .15;
});
summary && summary.select('rect')
.attr('stroke', function(d) {
return d.hovered ? '#FFD84B' : 'none';
});
hoveredData && selected[hoveredData.id] && summaries.transition().duration(duration * 3)
.attr('transform', function() {
var x = width + margin.left + margin.right - hoveredData.summaryX;
return 'translate(' + x + ',0)';
});
}
// load data
d3.json('data.json', function(data) {
var linkStrengths = [];
blocks = _.chain(data)
.uniq(function(block) {return block.id})
.reduce(function(memo, block) {
memo[block.id] = block;
return memo;
}, {}).value();
api = _.chain(data)
.pluck('api')
.map(function(api) {return _.pairs(api)})
.flatten().compact()
.filter(function(api) {return !_.contains(ignoreApi, api[0])})
.reduce(function(memo, api) {
linkStrengths.push(api[1]);
if (!memo[api[0]]) {
memo[api[0]] = 0;
}
memo[api[0]] += api[1];
return memo;
}, {})
.value();
totalApiSize = _.size(api);
colors = d3.scale.category20();
sizeScale = d3.scale.linear()
.domain([_.min(api), _.max(api)])
.range([7.5, 35]);
widthScale = d3.scale.linear()
.domain([_.min(linkStrengths), _.max(linkStrengths)])
.range([1, 4]);
strengthScale = d3.scale.linear()
.domain([_.min(linkStrengths), _.max(linkStrengths)])
.range([0, 1]);
apiNodes = _.chain(api)
.map(function(count, name) {
api[name] = {
id: name,
size: sizeScale(count),
fill: colors(name),
fixed: true,
type: 'api'
};
return api[name];
}).sortBy(function(node) {
return -node.size;
}).map(function(node, i) {
if (i > (apiSize - 1)) {
delete api[node.id];
return;
}
var radian = (2 * Math.PI) / apiSize * i - (Math.PI / 2);
node.x = radius * Math.cos(radian) + (width / 2);
node.y = radius * Math.sin(radian) + (height / 2);
return node;
}).compact().value();
blockNodes = _.chain(data)
.filter(function(block) {
// only keep if it has API's and at least one of them are in apiNodes
return !_.isEmpty(block.api) && _.some(block.api, function(count, apiName) {
return api[apiName];
});
}).uniq(function(block) {return block.id})
.map(function(block) {
var node = {
id: block.id,
title: block.description,
user: block.owner.login,
image: block.thumbnail,
size: 4,
fill: '#666'
};
var totalCount = _.reduce(block.api, function(memo, count, apiName) {
return memo + (api[apiName] ? count : 0);
}, 0);
_.each(block.api, function(count, apiName) {
if (!api[apiName]) return;
links.push({
source: node,
target: api[apiName],
size: widthScale(count),
strength: (.75 / totalCount) * count
});
});
return node;
}).value();
nodes = _.union(apiNodes, blockNodes);
force.nodes(nodes).links(links);
enterGraph();
force.start();
});
</script>
</body>