Skip to content

Instantly share code, notes, and snippets.

@mmazanec22
Created August 18, 2018 14:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mmazanec22/6f5a800c33449b5568b1284c6b12a257 to your computer and use it in GitHub Desktop.
Save mmazanec22/6f5a800c33449b5568b1284c6b12a257 to your computer and use it in GitHub Desktop.
Community Network Mapping Example
// 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 = [
]
<!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