Arc Bubble Connection Chart displaying graph data.
The data is a small sample of Lobbyist Activity from http://sfgov.org.
Arc Bubble Connection Chart displaying graph data.
The data is a small sample of Lobbyist Activity from http://sfgov.org.
/* | |
* Arc Bubble Connector (ABC) Chart | |
* | |
* Matt Hill - [email protected] | |
*/ | |
'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" | |
} | |
] | |
} |