Created
August 18, 2018 14:56
-
-
Save mmazanec22/6f5a800c33449b5568b1284c6b12a257 to your computer and use it in GitHub Desktop.
Community Network Mapping Example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// TODO: RECREATE THIS IN GOOGLE SHEET, EXPORT TO CSV FILES, PARSE WITH D3 | |
const organizationsDetail = [ | |
{ | |
name: 'City of Asheville', | |
type: 'government', | |
numMembers: 1300, | |
yearFounded: 1797, | |
}, | |
{ | |
name: 'Code for Asheville', | |
type: 'nonprofit', | |
numMembers: 30, | |
yearFounded: 2011, | |
}, | |
{ | |
name: 'Asheville Symphony Chorus', | |
type: 'nonprofit', | |
tags: ['arts', 'music'], | |
numMembers: 130, | |
yearFounded: 1980, | |
} | |
] | |
const people = [ | |
{ | |
name: 'Melanie', | |
orgMemberships: [ | |
{ | |
name: 'City of Asheville', | |
role: 'Software Engineer', | |
}, | |
{ | |
name: 'Asheville Symphony Chorus', | |
role: 'Alto', | |
}, | |
{ | |
name: 'Code for Asheville', | |
role: 'Volunteer', | |
}, | |
{ | |
name: 'Dev Bootcamp', | |
role: 'alumna' | |
} | |
], | |
}, | |
{ | |
name: 'Eric', | |
orgMemberships: [ | |
{ | |
name: 'City of Asheville', | |
role: 'Digital Services Architect', | |
}, | |
{ | |
name: 'Code for Asheville', | |
role: 'Former Co-Captain', | |
}, | |
{ | |
name: 'Circus', | |
role: 'clown' | |
} | |
] | |
}, | |
{ | |
name: 'Sarah', | |
orgMemberships: [ | |
{ | |
name: 'Asheville Symphony Chorus', | |
role: 'Alto', | |
}, | |
{ | |
name: 'Nobel Laureates', | |
role: 'winner' | |
} | |
] | |
}, | |
{ | |
name: 'Annie', | |
orgMemberships: [ | |
{ | |
name: 'MyCincinnatti', | |
role: 'teacher', | |
}, | |
{ | |
name: 'Habitat for Humanity', | |
role: 'Americorps' | |
} | |
] | |
} | |
] | |
// Assume members of the same organization are linked | |
// Other kinds of links: personal, professional, ? | |
const nonOrgPeopleLinks = [ | |
{ | |
source: 'Annie', | |
target: 'Melanie', | |
type: 'personal', | |
}, | |
] | |
// Assume orgs are linked through people | |
// Other kinds of links: partnerships, space sharing, subsidiaries, etc | |
// Should we generate orgs from the people list? | |
const nonPeopleOrgLinks = [ | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en" dir="ltr"> | |
<head> | |
<meta charset="utf-8"> | |
<title></title> | |
<style media="screen"> | |
html, body { | |
width: 100%; | |
height: 100%; | |
margin: 0 auto; | |
font-family: sans-serif; | |
font-weight: lighter; | |
text-align: center; | |
} | |
h1 { | |
color: lightseagreen; | |
} | |
h2 { | |
color: salmon; | |
} | |
.network { | |
height: 80vh; | |
width: 31%; | |
display: inline-block; | |
} | |
svg { | |
height: 100%; | |
width: 100%; | |
} | |
.nodes circle { | |
cursor: pointer; | |
} | |
</style> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script type="text/javascript" src="./data.js"></script> | |
</head> | |
<body> | |
<h1>Community Mapping Example</h1> | |
<div class="network" id="org-nodes"> | |
<h2>Organizations</h2> | |
<svg> | |
</svg> | |
</div> | |
<div class="network" id="individual-nodes"> | |
<h2>Individuals</h2> | |
<svg> | |
</svg> | |
</div> | |
<div class="network" id="circlepack-nodes"> | |
<h2>Both</h2> | |
<svg> | |
</svg> | |
</div> | |
</body> | |
<script type="text/javascript"> | |
const peopleLinks = nonOrgPeopleLinks; | |
const orgLinks = nonPeopleOrgLinks; | |
const organizations = organizationsDetail; | |
// For each person, iterate through other people links for people in the same org, create a link for each person | |
// TODO: dedupe this | |
people.forEach(person => { | |
person.orgMemberships.forEach(org => { | |
// If org is not in org list, add it | |
const isInOrgList = organizations.map(d => d.name).includes(org.name); | |
if (!isInOrgList) organizations.push({ | |
name: org.name, | |
type: 'unknown' | |
}) | |
// for each org that person is a member of, make a link to the other orgs they are also a member of | |
person.orgMemberships.forEach(candidateOrg => { | |
if (candidateOrg !== org) { | |
orgLinks.push({ | |
source: org.name, | |
target:candidateOrg.name, | |
}) | |
} | |
}) | |
people.forEach(candidatePerson => { | |
const colleague = candidatePerson.name !== person.name && candidatePerson.orgMemberships.map(o => o.name).includes(org.name) | |
if (colleague) { | |
peopleLinks.push({ | |
source: person.name, | |
target: candidatePerson.name, | |
type: 'organization' | |
}) | |
} | |
}) | |
}) | |
}) | |
// Add to orgs from list of a person's orgs | |
// Add to org links from people who are linked | |
const orgSvg = d3.select('#org-nodes').select('svg') | |
const peopleSvg = d3.select('#individual-nodes').select('svg') | |
const bothSvg = d3.select('#circlepack-nodes').select('svg') | |
const width = orgSvg.style('width').replace('px', ''); | |
const height = orgSvg.style('height').replace('px', ''); | |
const orgSimulation = d3.forceSimulation() | |
.nodes(organizations); | |
const peopleSimulation = d3.forceSimulation() | |
.nodes(people) | |
const peopleLinkForce = d3.forceLink(peopleLinks) | |
.distance(100) | |
.id(d => d.name); | |
const orgLinkForce = d3.forceLink(orgLinks) | |
.distance(100) | |
.id(d => d.name); | |
peopleSimulation | |
.force("charge_force", d3.forceManyBody()) | |
.force("center_force", d3.forceCenter(width / 2, height / 2)) | |
.force("links",peopleLinkForce); | |
orgSimulation | |
.force("charge_force", d3.forceManyBody()) | |
.force("center_force", d3.forceCenter(width / 2, height / 2)) | |
.force("links",orgLinkForce); | |
//add tick instructions: | |
peopleSimulation.on("tick", tickActions ); | |
orgSimulation.on("tick", tickActions ); | |
const peopleLinkEls = peopleSvg.append("g") | |
.attr("class", "links") | |
.selectAll("line") | |
.data(peopleLinks) | |
.enter().append("line") | |
.style('stroke', 'lightseagreen') | |
.attr("stroke-width", 2); | |
const orgLinkEls = orgSvg.append("g") | |
.attr("class", "links") | |
.selectAll("line") | |
.data(orgLinks) | |
.enter().append("line") | |
.style('stroke', 'lightseagreen') | |
.attr("stroke-width", 2); | |
const tooltipText = makeToolTip() | |
const peopleNodes = peopleSvg.append("g") | |
.attr("class", "nodes") | |
.selectAll("circle") | |
.data(people) | |
.enter() | |
.append("circle") | |
.attr("r", 10) | |
.attr('stroke', 'salmon') | |
.attr('stroke-width', 2) | |
.attr("fill", "white") | |
.on('mouseover', d => tooltipText.text(d.name)) | |
.on('mouseout', d => tooltipText.text('')) | |
const orgNodes = orgSvg.append("g") | |
.attr("class", "nodes") | |
.selectAll("circle") | |
.data(organizations) | |
.enter() | |
.append("circle") | |
.attr("r", 10) | |
.attr('stroke-width', 2) | |
.attr('stroke', 'salmon') | |
.attr("fill", "white") | |
.on('mouseover', d => tooltipText.text(d.name)) | |
.on('mouseout', d => tooltipText.text('')) | |
const dragHandler = d3.drag() | |
.on("start", dragStart) | |
.on("drag", dragDrag) | |
.on("end", dragEnd); | |
dragHandler(peopleNodes) | |
dragHandler(orgNodes) | |
function dragStart(d) { | |
if (!d3.event.active) { | |
peopleSimulation.alphaTarget(0.3 ).restart(); | |
orgSimulation.alphaTarget(0.3 ).restart(); | |
} | |
d.fx = d.x; | |
d.fy = d.y; | |
} | |
function dragDrag(d) { | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
} | |
function dragEnd(d) { | |
if (!d3.event.active) { | |
peopleSimulation.alphaTarget(0); | |
orgSimulation.alphaTarget(0); | |
} | |
d.fx = d.x; | |
d.fy = d.y; | |
} | |
function tickActions() { | |
peopleNodes | |
.attr("cx", function(d) { return d.x; }) | |
.attr("cy", function(d) { return d.y; }); | |
orgNodes | |
.attr("cx", function(d) { return d.x; }) | |
.attr("cy", function(d) { return d.y; }); | |
peopleLinkEls | |
.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; }); | |
orgLinkEls | |
.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; }); | |
} | |
function makeToolTip() { | |
const body = d3.select('body'); | |
const tooltip = d3.select('body').append('div') | |
.attr('class', 'tooltip') | |
.style('position', 'absolute') | |
.style('z-index', '1'); | |
tooltip.append('text') | |
.text('') | |
.attr('text-anchor', 'middle') | |
.style('color', 'lightseagreen'); | |
body | |
.on('mousemove', () => { | |
moveToolTip(tooltip); | |
}) | |
.on('mouseout', () => { | |
tooltip.style('display', 'none'); | |
}); | |
return tooltip.select('text'); | |
} | |
function moveToolTip(tooltip) { | |
const body = d3.select('body'); | |
tooltip.style('display', 'unset'); | |
const svgDimensions = body.node().getBoundingClientRect(); | |
const eventXRelToScroll = d3.event.pageX - window.scrollX; | |
const eventYRelToScroll = d3.event.pageY - window.scrollY; | |
let tipX = (eventXRelToScroll) + 10; | |
let tipY = (eventYRelToScroll) + 5; | |
const tooltipDimensions = tooltip.node().getBoundingClientRect(); | |
tipX = (eventXRelToScroll + tooltipDimensions.width + 10 > svgDimensions.right) ? | |
tipX - tooltipDimensions.width - 10 : tipX; | |
tipY = (eventYRelToScroll + tooltipDimensions.height + 5 > svgDimensions.bottom) ? | |
tipY - tooltipDimensions.height - 5 : tipY; | |
tooltip | |
.transition() | |
.duration(10) | |
.style('top', `${tipY}px`) | |
.style('left', `${tipX}px`); | |
} | |
</script> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment