Skip to content

Instantly share code, notes, and snippets.

@hanshenSun
Created April 27, 2018 18:09
Show Gist options
  • Save hanshenSun/579ccb7c6b28a5bee16bf68d7860754d to your computer and use it in GitHub Desktop.
Save hanshenSun/579ccb7c6b28a5bee16bf68d7860754d to your computer and use it in GitHub Desktop.
Arc Bubble Connector Chart
license: mit
/*
* Arc Bubble Connector (ABC) Chart
*
* Matt Hill - matt@hyperceptive.org
*/
'use strict';
//Constants
var NODE_TYPE_1 = '#27aae1', //blue
NODE_TYPE_2 = '#f57c22', //orange
NODE_TYPE_3 = '#05a9a9', //green
NODE_TYPE_4 = '#b48441'; //orange
var ARC_STROKE = '#231f20',
CONNECTOR_STROKE = '#231f20',
CALLOUT_BACKGROUND = '#DDD';
var ARC_STROKE_OFF_OPACITY = 0.9,
BUBBLE_FILL_ON_OPACITY = 0.8,
BUBBLE_FILL_OFF_OPACITY = 0.5,
CONNECTOR_ARC_ON_OPACITY = 0.9,
CONNECTOR_ARC_OFF_OPACITY = 0.0,
CONNECTOR_ON_OPACITY = 0.9,
CONNECTOR_OFF_OPACITY = 0.22,
CONNECTOR_STROKE_ON_OPACITY = 0.9,
CONNECTOR_STROKE_OFF_OPACITY = 0.4;
var TOOLTIP_OPACITY = '0.9';
var DEGREES_90 = 1.57079633;
//Scope variables for data
var arcs = [],
arcsById = {},
arcData = [],
arcDataById = {},
bubbleData = [],
bubblesById = {},
relationships = [],
relationshipsByArcId = {},
relationshipsByBubbleId = {};
function getGroupColor(group) {
var color = '#AAA';
switch(parseInt(group)) {
case 1:
color = NODE_TYPE_1;
break;
case 2:
color = NODE_TYPE_2;
break;
case 3:
color = NODE_TYPE_3;
break;
case 4:
color = NODE_TYPE_4;
break;
}
return color;
}
/**
* Calculate the sizes of the arcs around the outside of the circle.
*
* Also setup arcsById
*
*/
function prepareArcs() {
arcs = [];
arcsById = {};
var matrix = [];
var idByIndex = [];
var nameByIndex = [];
var indexByName = [];
var n = 0;
//Create unique indexes for the arcs
arcData.forEach(function(d) {
if(!(d.name in indexByName)) {
idByIndex[n] = d.id;
nameByIndex[n] = d.name;
indexByName[d.name] = n++;
}
});
//Build matrix for the Chord layout
arcData.forEach(function(d) {
var source = indexByName[d.name],
row = matrix[source];
if(!row) {
row = matrix[source] = [];
for(var j = -1; ++j < n; ) {
row[j] = 0;
}
}
row[indexByName[d.name]] = Number(d.value);
});
//Set the data for the chord layout
chordLayout.matrix(matrix);
arcs = chordLayout.chords();
arcs.forEach(function (d, i) {
d.id = idByIndex[i];
d.label = nameByIndex[i];
d.angle = (d.source.startAngle + d.source.endAngle) / 2;
var o = {};
o.startAngle = d.source.startAngle;
o.endAngle = d.source.endAngle;
o.index = d.source.index;
o.value = d.source.value;
o.currentAngle = d.source.startAngle;
o.currentConnectorAngle = d.source.startAngle;
o.source = d.source;
o.relatedConnectors = [];
arcsById[d.id] = o;
i++;
});
for(var key in arcsById) {
if(arcsById.hasOwnProperty(key) && relationshipsByArcId.hasOwnProperty(key)) {
arcsById[key].relatedConnectors = relationshipsByArcId[key];
}
}
}
/**
* Setup and draw the arcs
*
*/
function updateArcs() {
var arcGroup = arcsSvg.selectAll('g.arcs')
.data(arcs, function (d) {
return d.id + '_' + d.angle;
});
var enter = arcGroup.enter().append('g').attr('class', 'arcs');
enter.append('text')
.attr('class', 'arc')
.attr('dx', function (d, i) {
var angle = d.angle * 180 / Math.PI;
//If label is on right, go right.
if(angle < 180) {
return '10px';
}
else {
return '-10px';
}
})
.attr('dy', '3px')
.attr('dummyAngle', function (d, i) {
var angle = d.angle * 180 / Math.PI;
return angle;
})
.style('fill', function(d) {
return '#231f20';
//fish: return getGroupColor(arcDataById[d.id].group);
})
.on('mouseover', function (d) { onMouseOver(d, 'arc'); })
.on('mouseout', function (d) { onMouseOut(d, 'arc'); });
//arc outline
enter.append('path')
.attr('class', 'arcOutline')
.style('fill-opacity', 0)
.style('stroke', ARC_STROKE)
.style('stroke-width', 1)
.style('stroke-opacity', ARC_STROKE_OFF_OPACITY)
.style('stroke-dasharray', function(d) {
return ('5, 5');
})
.attr('d', function (d, i) {
var arc = d3.svg.arc(d, i).innerRadius(innerRadius - 20).outerRadius(innerRadius);
return arc(d.source, i);
});
enter.append('circle')
.attr('class', 'arcLabelDot')
.style('fill', function(d) {
return getGroupColor(arcDataById[d.id].group);
})
.style('fill-opacity', 0.9)
.attr('r', function(d) {
return 2;
})
.attr('transform', function(d) {
var x = ((innerRadius + 17) * Math.cos(d.angle - DEGREES_90));
var y = ((innerRadius + 17) * Math.sin(d.angle - DEGREES_90));
return 'translate(' + x + ',' + y + ')';
});
arcGroup.selectAll('text')
//level labels
.attr('text-anchor', function(d) { return d.angle > Math.PI ? 'end' : null; })
.attr('transform', function(d) {
var x = ((innerRadius + 20) * Math.cos(d.angle - DEGREES_90));
var y = ((innerRadius + 20) * Math.sin(d.angle - DEGREES_90));
return 'translate(' + x + ',' + y + ')';
})
.text(function(d) { return d.label; })
.attr('id', function (d) { return 't_' + d.id; });
arcGroup.exit().remove();
}
/**
* Get ready to draw the bubbles.
*
*/
function prepareBubbles() {
var bubbles = [],
root = {};
root.children = bubbleData;
bubbles = bubbleLayout.nodes(root);
bubbles.forEach(function (d) {
if(d.depth === 1) {
d.relatedConnectors = relationshipsByBubbleId[d.id];
}
});
}
/**
* Setup and draw the Bubbles (Circles) in the middle
*
* Enter-Update-Exit Pattern
*
*/
function updateBubbles() {
//1: update the data
var bubbleGroup = bubblesSvg.selectAll('g.bubble')
.data(bubbleData, function (d, i) {
//if any of these values change, consider it a key change.
return d.id + '_' + d.value + '_' + d.r;
});
//2: Operate only on existing elements - currently nothing.
//3: Operate only on new elements.
var enter = bubbleGroup.enter().append('g').attr('class', 'bubble');
enter.append('circle')
.attr('class', 'bubble')
.attr('id', function(d) { return 'b_' + d.id; })
.style('fill', function(d) {
return getGroupColor(d.group);
})
.style('fill-opacity', BUBBLE_FILL_OFF_OPACITY)
.on('mouseover', function (d) { onMouseOver(d, 'bubble'); })
.on('mouseout', function (d) { onMouseOut(d, 'bubble'); })
.attr('r', function (d) { return 0; })
.transition()
.duration(800)
.ease('elastic') //cubic, elastic, bounce, linear
.attr('r', function (d) { return d.r - 1; });
enter.append('circle')
.attr('class', 'bubbleCenter')
.style('fill', function(d) { return '#FFFFFF'; })
.style('fill-opacity', 1)
.attr('r', function(d) {
if(d.r > 75) {
return 3;
}
return 2;
});
//For the hover highlight
var g = enter.append('g')
.attr('id', function(d) { return 'bh_' + d.id; })
.style('opacity', 0);
g.append('circle')
.attr('class', 'bubbleCenterHighlight')
.style('fill', function(d) { return '#FFF'; })
.style('fill-opacity', 1);
//4: Operate on new and existing elements
bubbleGroup.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
bubbleGroup.selectAll('.bubbleCenterHighlight')
.attr('r', function(d) { return 4; });
//5: complete the enter-update-exit pattern
bubbleGroup.exit().remove().transition().duration(500).style('opacity', 0);
}
function updateBubbleLabels() {
//1: update the data
var bubbleLabelsGroup = bubbleLabelsSvg.selectAll('g.bubbleLabels')
.data(bubbleData, function (d, i) {
//if any of these values change, consider it a key change.
return d.id + '_' + d.value + '_' + d.r;
});
//2: Operate only on existing elements - currently nothing.
//3: Operate only on new elements.
var enter = bubbleLabelsGroup.enter().append('g').attr('class', 'bubbleLabels');
//Label in Callout
var callout = enter.append('g');
enter.append('text')
.attr('id', function(d) { return 't_' + d.id.toString().replace(/&/g, ''); })
.text(function(d) { return d.name; })
.attr('dy', function (d, i) {
//Change yOffset based on height.
var returnVal = '-3px'; //small text
var height = this.getBBox().height;
if(height >= 22) {
returnVal = '-5px'; //big text
}
else if(height > 15) {
returnVal = '-4px';
}
return returnVal;
})
.attr('dx', function (d, i) {
d.textLength = this.getComputedTextLength(); //save fer later
d.textHeight = this.getBBox().height;
//If center x point is greater than middle, go right.
if(d.x > bubbleRadius) {
return '35px';
}
else {
var xOffset = d.textLength + 35;
return '-' + xOffset + 'px';
}
});
callout.append('path')
.attr('id', function(d) { return 'p_' + d.id.toString().replace(/&/g, ''); })
.style('fill', CALLOUT_BACKGROUND)
.style('stroke', CALLOUT_BACKGROUND)
.style('stroke-width', 1.5)
.attr('class', 'bubbleLabel')
.attr('d', function (d, i) {
//If center x point is greater than middle, go right.
var hLength = d.textLength + 40;
var CALLOUT_HEIGHT = 25 * (d.textHeight / 22);
if(d.x > bubbleRadius) {
return 'M 0,0 L ' + hLength + ',0 L ' + hLength + ',-' + CALLOUT_HEIGHT + ' L 35,-' + CALLOUT_HEIGHT + ' L 15,0 Z';
}
else {
return 'M 0,0 L -' + hLength + ',0 L -' + hLength + ',-' + CALLOUT_HEIGHT + ' L -35,-' + CALLOUT_HEIGHT + ' L -15,0 Z';
}
});
//4: Operate on new and existing elements
bubbleLabelsGroup.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
//5: complete the enter-update-exit pattern
bubbleLabelsGroup.exit().remove().transition().duration(500).style('opacity', 0);
}
function drawArc(d, i) {
var newArc = {};
var relatedArc = arcsById[d.arcId];
var relatedArcData = arcDataById[d.arcId];
//Start and end angle are based on the data.
newArc.startAngle = relatedArc.currentAngle;
relatedArc.currentAngle = relatedArc.currentAngle + (Number(1) / relatedArcData.value) * (relatedArc.endAngle - relatedArc.startAngle);
newArc.endAngle = relatedArc.currentAngle;
//Inner and outer radius are fixed.
var arc = d3.svg.arc(d, i).innerRadius(connectorRadius).outerRadius(innerRadius);
return arc(newArc);
}
/**
* Setup and draw the Connectors between the Arcs and the Bubbles.
*
* Currently, Connectors are removed when data is updated, so we aren't (yet) using Enter-Update-Exit Pattern.
*
*/
function updateConnectors(connectors) {
function createConnectors(d) {
var target = {};
var source = {};
var connector = {};
var connector2 = {};
var source2 = {};
var relatedArc = arcsById[d.arcId];
var relatedArcData = arcDataById[d.arcId];
var relatedBubble = bubblesById[d.bubbleId];
var r = connectorRadius;
var currX = (r * Math.cos(relatedArc.currentConnectorAngle - DEGREES_90));
var currY = (r * Math.sin(relatedArc.currentConnectorAngle - DEGREES_90));
var a = relatedArc.currentConnectorAngle - DEGREES_90;
relatedArc.currentConnectorAngle = relatedArc.currentConnectorAngle + (Number(1) / relatedArcData.value) * (relatedArc.endAngle - relatedArc.startAngle);
var a1 = relatedArc.currentConnectorAngle - DEGREES_90;
source.x = (r * Math.cos(a));
source.y = (r * Math.sin(a));
target.x = relatedBubble.x - (arcsTranslateX - bubblesTranslateX);
target.y = relatedBubble.y - (arcsTranslateY - bubblesTranslateY);
source2.x = (r * Math.cos(a1));
source2.y = (r * Math.sin(a1));
connector.source = source;
connector.target = target;
connector2.source = target;
connector2.target = source2;
return [connector, connector2];
}
//1: update the data
var connectorGroup = connectorsSvg.selectAll('g.connectors')
.data(connectors, function (d, i) {
return d.id;
});
//2: Operate only on existing elements -- Currently nothing.
//3: Operate only on new elements.
var enter = connectorGroup.enter().append('g').attr('class', 'connectors');
// Arcs
enter.append('g')
.append('path')
.attr('class', 'arc')
.attr('id', function(d) { return 'a_' + d.id; })
.style('fill', function(d) {
return getGroupColor(arcDataById[d.arcId].group);
})
.style('fill-opacity', CONNECTOR_ARC_OFF_OPACITY)
.attr('d', function (d, i) {
return drawArc(d, i);
})
.on('mouseover', function (d) { onMouseOver(d, 'connector'); })
.on('mouseout', function (d) { onMouseOut(d, 'connector'); });
// Connectors between Arcs and Bubbles
enter.append('path')
.attr('class', 'connector')
.attr('id', function (d) { return 'c_' + d.id; })
.style('stroke', CONNECTOR_STROKE)
.style('stroke-width', 1)
.style('stroke-opacity', CONNECTOR_STROKE_OFF_OPACITY)
.style('fill', function(d) {
return getGroupColor(arcDataById[d.arcId].group);
})
.style('fill-opacity', CONNECTOR_OFF_OPACITY)
.attr('d', function (d, i) {
d.connectors = createConnectors(d);
var diag = diagonal(d.connectors[0], i);
diag += 'L' + String(diagonal(d.connectors[1], i)).substr(1);
diag += 'A' + (connectorRadius) + ',' + (connectorRadius) + ' 0 0, 0 ' + d.connectors[0].source.x + ',' + d.connectors[0].source.y;
return diag;
})
.on('mouseover', function (d) { onMouseOver(d, 'connector'); })
.on('mouseout', function (d) { onMouseOut(d, 'connector'); });
//5: Complete the enter-update-exit pattern
connectorGroup.exit().remove();
}
//**************************
// Event Handling
//**************************
function onMouseOver(d, type) {
var arcHideList = [];
var bubbleHideList = [];
//highlight this bubble and all arcs and connectors coming to this bubble.
if(type === 'bubble') {
if(d.depth < 1) { return; }
//Hide any Arc labels not connected to this bubble.
for(var key in arcsById) {
if(arcsById.hasOwnProperty(key)) {
var found = false;
for(var i = 0; i < d.relatedConnectors.length; i++) {
if(key === d.relatedConnectors[i].arcId.toString()) {
found = true;
break;
}
}
if(!found) {
arcHideList.push(key);
}
}
}
//Hide all bubble labels, except this one.
for(var key1 in bubblesById) {
if(bubblesById.hasOwnProperty(key1)) {
if(key1 !== d.id.toString()) {
bubbleHideList.push(key1);
}
}
}
highlightConnectors(d, true);
}
//highlight this connectors and the corresponding arc and bubble.
else if(type === 'connector') {
//Hide all Arc labels, except the one tied to this connector.
for(var key2 in arcsById) {
if(arcsById.hasOwnProperty(key2)) {
if(key2 !== d.arcId.toString()) {
arcHideList.push(key2);
}
}
}
//Hide all bubble labels, except this one.
for(var key3 in bubblesById) {
if(bubblesById.hasOwnProperty(key3)) {
if(key3 !== d.bubbleId.toString()) {
bubbleHideList.push(key3);
}
}
}
highlightConnector(d, true);
}
//highlight all bubbles and connectors coming from this arc.
else if(type === 'arc') {
//Hide all Arc labels, except the one tied to this connector.
for(var key4 in arcsById) {
if(arcsById.hasOwnProperty(key4)) {
if(key4 !== d.id.toString()) {
arcHideList.push(key4);
}
}
}
//Only hide bubble labels not linked to this arc.
var relatedConnectors = arcsById[d.id].relatedConnectors;
for(var key5 in bubblesById) {
if(bubblesById.hasOwnProperty(key5)) {
var afound = false;
for(var j = 0; j < relatedConnectors.length; j++) {
if(key5 === relatedConnectors[j].bubbleId.toString()) {
afound = true;
break;
}
}
if(!afound) {
bubbleHideList.push(key5);
}
}
}
highlightConnectors(arcsById[d.id], true);
}
hideArcLabels(arcHideList, true);
hideBubbleLabels(bubbleHideList, true);
}
function onMouseOut(d, type) {
var arcShowList = [];
for(var akey in arcsById) {
if(arcsById.hasOwnProperty(akey)) {
arcShowList.push(akey);
}
}
hideArcLabels(arcShowList, false);
var bubbleShowList = [];
for(var bkey in bubblesById) {
if(bubblesById.hasOwnProperty(bkey)) {
bubbleShowList.push(bkey);
}
}
hideBubbleLabels(bubbleShowList, false);
if(type === 'bubble') {
highlightConnectors(d, false);
}
else if(type === 'connector') {
highlightConnector(d, false);
}
else if(type === 'arc') {
highlightConnectors(arcsById[d.id], false);
}
}
function hideArcLabels(labels, hide) {
labels.forEach(function(label) {
var arcText = d3.select(document.getElementById('t_' + label));
arcText.transition()
.duration((hide === true) ? 550 : 550)
.style('opacity', (hide === true) ? 0 : 1);
});
}
function hideBubbleLabels(labels, hide) {
labels.forEach(function(label) {
var bubbleText = d3.select(document.getElementById('t_' + label.toString().replace(/&/g, '')));
bubbleText.transition()
.duration((hide === true) ? 550 : 550)
.style('opacity', (hide === true) ? 0 : 1);
var bubbleCallout = d3.select(document.getElementById('p_' + label.toString().replace(/&/g, '')));
bubbleCallout.transition()
.duration((hide === true) ? 550 : 550)
.style('opacity', (hide === true) ? 0 : 1);
});
}
function highlightConnector(g, on) {
var bub = d3.select(document.getElementById('b_' + g.bubbleId));
bub.transition()
.duration((on === true) ? 75 : 550)
.style('fill-opacity', (on === true) ? BUBBLE_FILL_ON_OPACITY : BUBBLE_FILL_OFF_OPACITY);
var circ = d3.select(document.getElementById('bh_' + g.bubbleId));
circ.transition()
.duration((on === true) ? 75 : 550)
.style('opacity', ((on === true) ? 1 : 0));
var connector = d3.select(document.getElementById('c_' + g.id));
connector.transition()
.duration((on === true) ? 150 : 550)
.style('fill-opacity', (on === true) ? CONNECTOR_ON_OPACITY : CONNECTOR_OFF_OPACITY)
.style('stroke-opacity', (on === true) ? CONNECTOR_STROKE_ON_OPACITY : CONNECTOR_STROKE_OFF_OPACITY);
var arc = d3.select(document.getElementById('a_' + g.id));
arc.transition()
.duration((on === true) ? 300 : 550)
.style('fill-opacity', (on === true) ? CONNECTOR_ARC_ON_OPACITY : CONNECTOR_ARC_OFF_OPACITY);
var arcText = d3.select(document.getElementById('t_' + g.arcId));
arcText.transition()
.duration((on === true) ? 400 : 400)
.style('opacity', 1);
}
function highlightConnectors(g, on) {
g.relatedConnectors.forEach(function (d) {
highlightConnector(d, on);
});
}
[
{
"id": "Board Of Supervisors",
"name": "Board Of Supervisors",
"group": 2,
"value": 6
},
{
"id": "Mayor Office Of The",
"name": "Mayor Office Of The",
"group": 2,
"value": 6
},
{
"id": "Municipal Transportation Agency",
"name": "Municipal Transportation Agency",
"group": 2,
"value": 3
},
{
"id": "Planning, Department Of",
"name": "Planning, Department Of",
"group": 2,
"value": 3
},
{
"id": "Economic And Workforce Development, Office Of",
"name": "Economic And Workforce Development, Office Of",
"group": 2,
"value": 2
},
{
"id": "General Services Agency",
"name": "General Services Agency",
"group": 2,
"value": 1
}
]
{
"Board Of Supervisors": {
"id": "Board Of Supervisors",
"name": "Board Of Supervisors",
"group": 2,
"value": 6
},
"Mayor Office Of The": {
"id": "Mayor Office Of The",
"name": "Mayor Office Of The",
"group": 2,
"value": 6
},
"Economic And Workforce Development, Office Of": {
"id": "Economic And Workforce Development, Office Of",
"name": "Economic And Workforce Development, Office Of",
"group": 2,
"value": 2
},
"Municipal Transportation Agency": {
"id": "Municipal Transportation Agency",
"name": "Municipal Transportation Agency",
"group": 2,
"value": 3
},
"Planning, Department Of": {
"id": "Planning, Department Of",
"name": "Planning, Department Of",
"group": 2,
"value": 3
},
"General Services Agency": {
"id": "General Services Agency",
"name": "General Services Agency",
"group": 2,
"value": 1
}
}
[
{
"id": "San Francisco Chamber Of Commerce",
"name": "San Francisco Chamber Of Commerce",
"group": 1,
"value": 30
},
{
"id": "Committee On Jobs",
"name": "Committee On Jobs",
"group": 1,
"value": 10
},
{
"id": "Pacific Gas And Electric Company",
"name": "Pacific Gas And Electric Company",
"group": 1,
"value": 15
},
{
"id": "San Francisco Association Of Realtors",
"name": "San Francisco Association Of Realtors",
"group": 1,
"value": 3
},
{
"id": "Bergdavis Public Affairs",
"name": "Bergdavis Public Affairs",
"group": 1,
"value": 7
},
{
"id": "Baybio",
"name": "Baybio",
"group": 1,
"value": 2
}
]
{
"San Francisco Chamber Of Commerce": {
"id": "San Francisco Chamber Of Commerce",
"name": "San Francisco Chamber Of Commerce",
"group": 1,
"value": 19
},
"Committee On Jobs": {
"id": "Committee On Jobs",
"name": "Committee On Jobs",
"group": 1,
"value": 10
},
"Pacific Gas And Electric Company": {
"id": "Pacific Gas And Electric Company",
"name": "Pacific Gas And Electric Company",
"group": 1,
"value": 15
},
"San Francisco Association Of Realtors": {
"id": "San Francisco Association Of Realtors",
"name": "San Francisco Association Of Realtors",
"group": 1,
"value": 4
},
"Bergdavis Public Affairs": {
"id": "Bergdavis Public Affairs",
"name": "Bergdavis Public Affairs",
"group": 1,
"value": 7
},
"Baybio": {
"id": "Baybio",
"name": "Baybio",
"group": 1,
"value": 4
}
}
{
"San Francisco Chamber Of Commerce": {
"id": "San Francisco Chamber Of Commerce",
"name": "San Francisco Chamber Of Commerce",
"group": 1,
"value": 19
},
"Committee On Jobs": {
"id": "Committee On Jobs",
"name": "Committee On Jobs",
"group": 1,
"value": 10
},
"Pacific Gas And Electric Company": {
"id": "Pacific Gas And Electric Company",
"name": "Pacific Gas And Electric Company",
"group": 1,
"value": 15
},
"San Francisco Association Of Realtors": {
"id": "San Francisco Association Of Realtors",
"name": "San Francisco Association Of Realtors",
"group": 1,
"value": 4
},
"Bergdavis Public Affairs": {
"id": "Bergdavis Public Affairs",
"name": "Bergdavis Public Affairs",
"group": 1,
"value": 7
},
"Baybio": {
"id": "Baybio",
"name": "Baybio",
"group": 1,
"value": 4
}
}
!function(a,b){"function"==typeof define&&define.amd?define(["d3"],b):a.d3.promise=b(a.d3)}(this,function(a){var b=function(){function b(a,b){return function(){var c=Array.prototype.slice.call(arguments);return new Promise(function(d,e){var f=function(a,b){return a?void e(Error(a)):void d(b)};b.apply(a,c.concat(f))})}}var c={};return["csv","tsv","json","xml","text","html"].forEach(function(d){c[d]=b(a,a[d])}),c}();return a.promise=b,b});
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="d3.promise.min.js"></script>
<script src="arcBubbleConnector.js"></script>
<style>
body {
margin:0;position:fixed;top:0;right:0;bottom:0;left:0;
font-family: "Avenir", "Proxima Nova", "Museo Sans", Helvetica;
}
svg { width: 100%; height: 100%; }
.arc { cursor: pointer; }
.arcOutline { pointer-events: none; }
text.arc {
font-size: 12px;
font-weight: 500;
letter-spacing: 1px;
}
.bubble { cursor: pointer; }
.bubbleCenter, .bubbleCenterHighlight, .bubbleLabel { pointer-events: none; }
.bubbleLabels > text {
text-transform: uppercase;
font-size: 10px;
font-weight: bold;
fill: #231f20;
pointer-events: none;
}
</style>
</head>
<body>
<script>
var margin = {top: 0, right: 0, bottom: 0, left: 0};
var width = 960 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var connectorsSvg = svg.append('g').attr('class', 'connectors');
var arcsSvg = svg.append('g').attr('class', 'arcs');
var bubblesSvg = svg.append('g').attr('class', 'bubbles');
var bubbleLabelsSvg = svg.append('g').attr('class', 'bubbleLabels');
//Create Layouts
function packSort(a, b) {
return b.value - a.value;
}
var bubbleLayout = d3.layout.pack()
.sort(packSort)
.padding(2.5);
var chordLayout = d3.layout.chord()
.padding(0.06);
var diagonal = d3.svg.diagonal.radial();
//Size Everything
var outerRadius = (width > height ? height / 1.7 : width / 1.7);
var innerRadius = outerRadius - 120;
var bubbleRadius = innerRadius - 50;
var connectorRadius = innerRadius - 20;
var bubblesTranslateX = (outerRadius - innerRadius) + (innerRadius - bubbleRadius);
var bubblesTranslateY = (outerRadius - innerRadius) + (innerRadius - bubbleRadius);
var arcsTranslateX = outerRadius;
var arcsTranslateY = outerRadius;
//Move to middle of X.
var middle = width / 2;
var xOffset = 0,
yOffset = -50;
if(arcsTranslateX < middle) {
xOffset = middle - arcsTranslateX;
}
bubblesTranslateX += xOffset;
bubblesTranslateY += yOffset;
arcsTranslateX += xOffset;
arcsTranslateY += yOffset;
arcsSvg.attr('transform', 'translate(' + arcsTranslateX + ',' + arcsTranslateY + ')');
connectorsSvg.attr('transform', 'translate(' + arcsTranslateX + ',' + arcsTranslateY + ')');
bubblesSvg.attr('transform', 'translate(' + bubblesTranslateX + ',' + bubblesTranslateY + ')');
bubbleLabelsSvg.attr('transform', 'translate(' + bubblesTranslateX + ',' + bubblesTranslateY + ')');
bubbleLayout.size([bubbleRadius * 2, bubbleRadius * 2]);
var promises = [];
//Load Preprocessed Data
promises.push(d3.promise.json('arcData.json',
function(data) { arcData = data; }));
promises.push(d3.promise.json('arcDataById.json',
function(data) { arcDataById = data; }));
promises.push(d3.promise.json('bubbleData.json',
function(data) {
bubbleData = data;
bubblesById = {};
bubbleData.forEach(function(b) {
bubblesById[b.id] = b;
});
}));
promises.push(d3.promise.json('relationships.json',
function(data) { relationships = data; }));
promises.push(d3.promise.json('relationshipsByArcId.json',
function(data) { relationshipsByArcId = data; }));
promises.push(d3.promise.json('relationshipsByBubbleId.json',
function(data) { relationshipsByBubbleId = data; }));
function drawChart() {
//For some reason, not waiting for promises...
if(arcData.length === 0 || bubbleData.length === 0 || relationships.length === 0) {
console.log('waiting...');
setTimeout(drawChart, 100);
}
else {
prepareArcs();
prepareBubbles();
updateArcs();
updateConnectors(relationships);
updateBubbles();
updateBubbleLabels();
}
};
//Wait for all data to be loaded
$.when.apply($, promises).done(function() {
drawChart();
});
</script>
</body>
[
{
"id": "Board Of Supervisors_San Francisco Chamber Of Commerce",
"arcId": "Board Of Supervisors",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Board Of Supervisors_Committee On Jobs",
"arcId": "Board Of Supervisors",
"bubbleId": "Committee On Jobs"
},
{
"id": "Board Of Supervisors_Pacific Gas And Electric Company",
"arcId": "Board Of Supervisors",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Board Of Supervisors_San Francisco Association Of Realtors",
"arcId": "Board Of Supervisors",
"bubbleId": "San Francisco Association Of Realtors"
},
{
"id": "Board Of Supervisors_Bergdavis Public Affairs",
"arcId": "Board Of Supervisors",
"bubbleId": "Bergdavis Public Affairs"
},
{
"id": "Board Of Supervisors_Baybio",
"arcId": "Board Of Supervisors",
"bubbleId": "Baybio"
},
{
"id": "Mayor Office Of The_San Francisco Chamber Of Commerce",
"arcId": "Mayor Office Of The",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Mayor Office Of The_Committee On Jobs",
"arcId": "Mayor Office Of The",
"bubbleId": "Committee On Jobs"
},
{
"id": "Mayor Office Of The_Pacific Gas And Electric Company",
"arcId": "Mayor Office Of The",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Mayor Office Of The_San Francisco Association Of Realtors",
"arcId": "Mayor Office Of The",
"bubbleId": "San Francisco Association Of Realtors"
},
{
"id": "Mayor Office Of The_Bergdavis Public Affairs",
"arcId": "Mayor Office Of The",
"bubbleId": "Bergdavis Public Affairs"
},
{
"id": "Mayor Office Of The_Baybio",
"arcId": "Mayor Office Of The",
"bubbleId": "Baybio"
},
{
"id": "Economic And Workforce Development, Office Of_San Francisco Chamber Of Commerce",
"arcId": "Economic And Workforce Development, Office Of",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Economic And Workforce Development, Office Of_Committee On Jobs",
"arcId": "Economic And Workforce Development, Office Of",
"bubbleId": "Committee On Jobs"
},
{
"id": "Municipal Transportation Agency_San Francisco Chamber Of Commerce",
"arcId": "Municipal Transportation Agency",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Municipal Transportation Agency_Committee On Jobs",
"arcId": "Municipal Transportation Agency",
"bubbleId": "Committee On Jobs"
},
{
"id": "Municipal Transportation Agency_Pacific Gas And Electric Company",
"arcId": "Municipal Transportation Agency",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Planning, Department Of_San Francisco Chamber Of Commerce",
"arcId": "Planning, Department Of",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Planning, Department Of_Pacific Gas And Electric Company",
"arcId": "Planning, Department Of",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Planning, Department Of_Bergdavis Public Affairs",
"arcId": "Planning, Department Of",
"bubbleId": "Bergdavis Public Affairs"
},
{
"id": "General Services Agency_San Francisco Chamber Of Commerce",
"arcId": "General Services Agency",
"bubbleId": "San Francisco Chamber Of Commerce"
}
]
{
"Board Of Supervisors": [
{
"id": "Board Of Supervisors_San Francisco Chamber Of Commerce",
"arcId": "Board Of Supervisors",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Board Of Supervisors_Committee On Jobs",
"arcId": "Board Of Supervisors",
"bubbleId": "Committee On Jobs"
},
{
"id": "Board Of Supervisors_Pacific Gas And Electric Company",
"arcId": "Board Of Supervisors",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Board Of Supervisors_San Francisco Association Of Realtors",
"arcId": "Board Of Supervisors",
"bubbleId": "San Francisco Association Of Realtors"
},
{
"id": "Board Of Supervisors_Bergdavis Public Affairs",
"arcId": "Board Of Supervisors",
"bubbleId": "Bergdavis Public Affairs"
},
{
"id": "Board Of Supervisors_Baybio",
"arcId": "Board Of Supervisors",
"bubbleId": "Baybio"
}
],
"Mayor Office Of The": [
{
"id": "Mayor Office Of The_San Francisco Chamber Of Commerce",
"arcId": "Mayor Office Of The",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Mayor Office Of The_Committee On Jobs",
"arcId": "Mayor Office Of The",
"bubbleId": "Committee On Jobs"
},
{
"id": "Mayor Office Of The_Pacific Gas And Electric Company",
"arcId": "Mayor Office Of The",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Mayor Office Of The_San Francisco Association Of Realtors",
"arcId": "Mayor Office Of The",
"bubbleId": "San Francisco Association Of Realtors"
},
{
"id": "Mayor Office Of The_Bergdavis Public Affairs",
"arcId": "Mayor Office Of The",
"bubbleId": "Bergdavis Public Affairs"
},
{
"id": "Mayor Office Of The_Baybio",
"arcId": "Mayor Office Of The",
"bubbleId": "Baybio"
}
],
"Economic And Workforce Development, Office Of": [
{
"id": "Economic And Workforce Development, Office Of_San Francisco Chamber Of Commerce",
"arcId": "Economic And Workforce Development, Office Of",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Economic And Workforce Development, Office Of_Committee On Jobs",
"arcId": "Economic And Workforce Development, Office Of",
"bubbleId": "Committee On Jobs"
}
],
"Municipal Transportation Agency": [
{
"id": "Municipal Transportation Agency_San Francisco Chamber Of Commerce",
"arcId": "Municipal Transportation Agency",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Municipal Transportation Agency_Committee On Jobs",
"arcId": "Municipal Transportation Agency",
"bubbleId": "Committee On Jobs"
},
{
"id": "Municipal Transportation Agency_Pacific Gas And Electric Company",
"arcId": "Municipal Transportation Agency",
"bubbleId": "Pacific Gas And Electric Company"
}
],
"Planning, Department Of": [
{
"id": "Planning, Department Of_San Francisco Chamber Of Commerce",
"arcId": "Planning, Department Of",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Planning, Department Of_Pacific Gas And Electric Company",
"arcId": "Planning, Department Of",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Planning, Department Of_Bergdavis Public Affairs",
"arcId": "Planning, Department Of",
"bubbleId": "Bergdavis Public Affairs"
}
],
"General Services Agency": [
{
"id": "General Services Agency_San Francisco Chamber Of Commerce",
"arcId": "General Services Agency",
"bubbleId": "San Francisco Chamber Of Commerce"
}
]
}
{
"San Francisco Chamber Of Commerce": [
{
"id": "Board Of Supervisors_San Francisco Chamber Of Commerce",
"arcId": "Board Of Supervisors",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Mayor Office Of The_San Francisco Chamber Of Commerce",
"arcId": "Mayor Office Of The",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Economic And Workforce Development, Office Of_San Francisco Chamber Of Commerce",
"arcId": "Economic And Workforce Development, Office Of",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Municipal Transportation Agency_San Francisco Chamber Of Commerce",
"arcId": "Municipal Transportation Agency",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "Planning, Department Of_San Francisco Chamber Of Commerce",
"arcId": "Planning, Department Of",
"bubbleId": "San Francisco Chamber Of Commerce"
},
{
"id": "General Services Agency_San Francisco Chamber Of Commerce",
"arcId": "General Services Agency",
"bubbleId": "San Francisco Chamber Of Commerce"
}
],
"Committee On Jobs": [
{
"id": "Board Of Supervisors_Committee On Jobs",
"arcId": "Board Of Supervisors",
"bubbleId": "Committee On Jobs"
},
{
"id": "Mayor Office Of The_Committee On Jobs",
"arcId": "Mayor Office Of The",
"bubbleId": "Committee On Jobs"
},
{
"id": "Economic And Workforce Development, Office Of_Committee On Jobs",
"arcId": "Economic And Workforce Development, Office Of",
"bubbleId": "Committee On Jobs"
},
{
"id": "Municipal Transportation Agency_Committee On Jobs",
"arcId": "Municipal Transportation Agency",
"bubbleId": "Committee On Jobs"
}
],
"Pacific Gas And Electric Company": [
{
"id": "Board Of Supervisors_Pacific Gas And Electric Company",
"arcId": "Board Of Supervisors",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Mayor Office Of The_Pacific Gas And Electric Company",
"arcId": "Mayor Office Of The",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Municipal Transportation Agency_Pacific Gas And Electric Company",
"arcId": "Municipal Transportation Agency",
"bubbleId": "Pacific Gas And Electric Company"
},
{
"id": "Planning, Department Of_Pacific Gas And Electric Company",
"arcId": "Planning, Department Of",
"bubbleId": "Pacific Gas And Electric Company"
}
],
"San Francisco Association Of Realtors": [
{
"id": "Board Of Supervisors_San Francisco Association Of Realtors",
"arcId": "Board Of Supervisors",
"bubbleId": "San Francisco Association Of Realtors"
},
{
"id": "Mayor Office Of The_San Francisco Association Of Realtors",
"arcId": "Mayor Office Of The",
"bubbleId": "San Francisco Association Of Realtors"
}
],
"Bergdavis Public Affairs": [
{
"id": "Board Of Supervisors_Bergdavis Public Affairs",
"arcId": "Board Of Supervisors",
"bubbleId": "Bergdavis Public Affairs"
},
{
"id": "Mayor Office Of The_Bergdavis Public Affairs",
"arcId": "Mayor Office Of The",
"bubbleId": "Bergdavis Public Affairs"
},
{
"id": "Planning, Department Of_Bergdavis Public Affairs",
"arcId": "Planning, Department Of",
"bubbleId": "Bergdavis Public Affairs"
}
],
"Baybio": [
{
"id": "Board Of Supervisors_Baybio",
"arcId": "Board Of Supervisors",
"bubbleId": "Baybio"
},
{
"id": "Mayor Office Of The_Baybio",
"arcId": "Mayor Office Of The",
"bubbleId": "Baybio"
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment