Skip to content

Instantly share code, notes, and snippets.

@MatthewSchumwinger
Last active November 22, 2016 18:27
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 MatthewSchumwinger/205c8da5ef4c95306f9b to your computer and use it in GitHub Desktop.
Save MatthewSchumwinger/205c8da5ef4c95306f9b to your computer and use it in GitHub Desktop.
gathering

Map the travel patterns of many people to a single destination. Suggested uses:

  • professional conferences and conventions
  • family reunions
  • athletic tournaments

This framework was originally used plot the cross-country travel of 50 relatives to a family reunion. It can be readily applied to professional gatherings – as seen here with speakers attending the 2015 JSGeo conference.


#### acknowledgements Powered by D3.js and inspired by Mike Bostock's demonstration of D3's route mapping possibilities.

  • generated with yo angular generator version 0.11.1.
  • employs moveToFront function to expose buried circles on hover.
  • modified my favorite tooltip helper to position tooltips on circle even if other elements are clicked.

FIXMEs

  • fix styling of all viz and map elements to transition properly on zoom and on reset (currently using clumsy one-off fixes)
  • fix tooltip position to transition with zoom
  • arc and circle viz elements sometimes overlay side-menu text elements because of the nested structure of groups. Z-index does not work for SVG elements, so how can this be fixed?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title> JS.geo </title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
<script src="https://biglakedata.com/files/reunion/d3/queue.v1.min.js"></script>
<script type="text/javascript" src="tooltip.js"></script>
<link href=
"main.css" rel="stylesheet" type="text/css">
</head>
<body>
<p class="header">Who's presenting at <a href="http://www.jsgeo.com">JS.Geo</a>?</p>
<p class="subhead"> October 8, 2015 | Philadelphia, PA </p>
<!-- MAIN CHART -->
<script type="text/javascript" src="main.js"></script>
<p class="footer">This framework was originally used to plot the cross-country travel of 50 relatives to a family reunion. It can be readily applied to professional gatherings – as seen here with the <a href="http://www.jsgeo.com">2015 JSGeo conference</a>. Powered by D3.js and inspired by Mike Bostock's <a href="http://bl.ocks.org/mbostock/5851933">demonstration</a> of D3's route mapping possibilities. <a href="https://gist.github.com/MatthewSchumwinger/205c8da5ef4c95306f9b">Source code.</a></p>
</body>
<!-- GOOGLE ANALYTICS -->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-49939650-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })();
</script>
</html>
/*hyperlink settings*/
a:link {color: #000000; text-decoration: underline; }
a:active {color: rgb(70, 130, 180); text-decoration: underline; }
a:visited {color: steelblue; text-decoration: underline; }
a:hover {color: #ff0000; text-decoration: none; }
.header {
display: block;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
font-size: 64px;
font-weight: 300;
height: 78px;
letter-spacing: -2px;
margin: 10px 0 4px 10px;
tab-size: 2;
text-rendering: optimizelegibility;
width: 960px;
}
.subhead {
color: #555;
display: block;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
height: auto;
margin-left: 010px;
margin-bottom: 10px;
margin-top: 0;
tab-size: 2;
width: 960px;
}
.footer {
color: #7e7a7a;
display: block;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
height: auto;
margin-left: 010px;
margin-bottom: 10px;
tab-size: 2;
width: 960px;
}
.background {
fill: none;
pointer-events: all;
}
.chart {
position: relative;
top: 10px;
left: 0;
}
/*background map */
.graticule {
fill: none;
stroke: #777;
stroke-width: .5px;
stroke-opacity: .4;
}
.boundary {
fill: #bbb;
stroke: #fff;
stroke-width: .5px;
stroke-linejoin: round;
stroke-linecap: round;
}
.state-boundary {
fill: none;
stroke: #fff;
stroke-width: .5px;
stroke-linejoin: round;
stroke-linecap: round;
stroke-opacity: .5;
}
/*data viz*/
.origin {
fill: white;
stroke: red;
stroke-width: 1px;
}
.arc {
fill: none;
stroke: red;
stroke-width: 2px;
stroke-linecap: round;
}
.side-menu {
margin-top: 0;
margin-left: 10px;
border: none;
/*opacity: .9;*/
/*width: 140px;
height: 520px;*/
position: absolute;
top: 120px;
left: 0;
fill: rgba(255,255,255,.9);
}
.speaker-list {
fill: steelblue;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
font-size: 12px;
font-weight: 500;
margin:0 0 6px 10px;
text-rendering: optimizelegibility;
}
.name {
font-size: 14px;
}
.org , .place{
font-size: 12px;
color: #999;
}
/*tooltip using call to tooltip.js*/
.tooltip {
position: absolute;
text-align: center;
width: 125px;
height: auto;
padding: 10px;
background-color: rgba(255,255,255,0.9);
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
pointer-events: none;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
}
.tooltip img {
display: block;
margin-left: auto;
margin-right: auto;
margin-bottom: 10px;
width: 100px;
height: 100px;
border-radius: 100px;
}
/*hover data highlights */
.highlight circle {
fill: orange;
stroke: white;
stroke-width: 2;
r: 7;
}
.highlight path {
stroke: orange;
stroke-width: 3px;
stroke-linecap: round;
}
.highlight text{
fill: orange;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
font-size: 14px;
font-weight: bold;
margin: 0 0 6px 10px;
text-rendering: optimizelegibility;
}
// move elements to front
// http://tributary.io/tributary/3922684
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
var width = 960,
height = 500,
active = d3.select(null); //zoom
// set projection and map position
// tweek these settings for a nice display
var projection = d3.geo.albers() // popular alternative is geo.mercator()
.center([0, 40.0]) //[30, 45.0]
.scale(900) // 275
.rotate([97.8717, 0])
.translate([width / 2, height / 2])
.precision(.1);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "chart");
var graticule = d3.geo.graticule();
var dataset;
var destination = [-75.1667 , 39.9500]; // Philly lat lon
var map = svg.append("g")
.attr("class", "map");
var
vizGroup = svg.append("g").attr("class", "viz"),
countryGroup = map.append("g"),
stateGroup = map.append("g")
;
// plot world, states, and people data
queue()
.defer(d3.json, "world-50m.json")
.defer(d3.json, "us.json")
.defer(d3.csv, "people.csv")
.await(visualize);
function visualize(error, world, us, people){
if (error) return console.log(error);
dataset = people;
// draw backgound to for reset function
map.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", reset);
// draw graticules
map.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
// draw world
countryGroup
.selectAll("path")
.data(topojson.feature(world, world.objects.countries).features)
.enter().insert("path", ".graticule")
.attr("class", "boundary")
.attr("d", path);
// draw states
stateGroup
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
// .attr("d", path)
.attr("class", "state-boundary")
.attr("d", path);
// create a group for each person record
var groups = vizGroup.selectAll("g")
.data(dataset)
.enter()
.append("g")
.attr("class", function (d) {
return d.name;
})
.on("mouseover", function() {
d3.select(this).attr("class", "highlight");
// move to front
var sel = d3.select(this);
sel.moveToFront();
})
.on("mouseout", function (d) {
d3.select(this).attr("class", function () {
return d.name;});
})
;
// draw arcs from each record origin to destination
groups.append("path")
.attr("class", "arc")
.attr("d", function (d) {
var coordDepart = [ d.longitude, d.latitude ];
var coordArrive = destination;
return path({
type: "LineString",
coordinates: [coordDepart,coordArrive]
});
})
.call(d3.helper.tooltip(function(d, i){
return "<IMG SRC=" + "'" +d.avatar+ "'" + ">"+
"<div class='name'>" + d.name + "</div>"+
"<div class = 'org'>" + d.org + "</div>"+
"<div class = 'place'>" + d.place + "</div>"
}
));
// draw circles for each record origin
groups.append("circle")
.attr("cx", function(d) {
return projection([d.longitude, d.latitude])[0];
})
.attr("cy", function(d) {
return projection([d.longitude, d.latitude])[1];
})
.attr("r", function(d) { return 4 + (d.count * 2); })
.attr("class", "origin")
.call(d3.helper.tooltip(function(d, i){
return "<IMG SRC=" + "'" +d.avatar+ "'" + ">"+
"<div class='name'>" + d.name + "</div>"+
"<div class = 'org'>" + d.org + "</div>"+
"<div class = 'place'>" + d.place + "</div>"
}
))
.on("click", clicked); // zoom
function clicked(d) {
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var
cx = projection([d.longitude, d.latitude])[0],
cy = projection([d.longitude, d.latitude])[1],
scale = 3.75,
translate = [width / 2 - scale * cx, height / 2 - scale * cy];
// TODO how to link this to main.css?
d3.selectAll(".chart circle").transition()
.duration(1000) //750
.style("stroke-width", 1 / scale *1.5 + "px") // *1.5 is just a fudge
.attr("r", 6 / scale *1.5 )
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
d3.selectAll(".chart path").transition()
.duration(1000) //750
.style("stroke-width", 2 / scale *1.5 + "px") // *1.5 is just a fudge
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
// TODO how to link this to main.css?
function reset() {
active.classed("active", false);
active = d3.select(null);
d3.selectAll(".chart path, circle").transition()
.duration(1000) //750
.style("stroke-width", "1.5px") // this is a fudge
.attr("r", 6 )
.attr("transform", "");
}
// backgound for sidebar
groups.append("rect")
.attr("class", "side-menu")
.attr("y", function(d, i) { return (0 + (20 * i)); })
.attr("x", "0")
.attr("width", "150")
.attr("height", "20")
.call(d3.helper.tooltip(function(d, i){
return "<IMG SRC=" + "'" +d.avatar+ "'" + ">"
+"<div class='name'>" + d.name + "</div>"
+"<div class = 'org'>" + d.org + "</div>"
}
))
.on("click", clicked);
;
// add sidebar list of names for each person record
groups.append("text")
.attr("class", "speaker-list")
.attr("y", function(d, i) { return (15 + (20 * i)); })
.attr("x", "10")
.text(function(d) {return d.name;})
.call(d3.helper.tooltip(function(d, i){
return "<IMG SRC=" + "'" +d.avatar+ "'" + ">"+
"<div class='name'>" + d.name + "</div>"+
"<div class = 'org'>" + d.org + "</div>"+
"<div class = 'place'>" + d.place + "</div>"
}
))
.on("click", clicked);
;
};
d3.select(self.frameElement).style("height", height + "px");
// universal zoom and pan listener, disabled
// http://bl.ocks.org/d3noob/5189284
// var zoom = d3.behavior.zoom()
// .on("zoom",function() {
// d3.selectAll(".chart path, circle").attr("transform","translate("+
// d3.event.translate.join(",")+")scale("+d3.event.scale+")");
// d3.selectAll(".chart path, circle").selectAll("path")
// .attr("d", path.projection(projection));
// });
// svg.call(zoom)
name org avatar place latitude longitude count present orgUrl twitter
Matthew Amato Cesium http://www.jsgeo.com/img/speakers/matt_amato.jpg Philadelphia, PA 39.952584 -75.165222 1 y @matt_amato
Lauren Ancona City of Philadelphia http://www.jsgeo.com/img/speakers/lauren_ancona.png Philadelphia, PA 39.952584 -75.165222 1 y @laurenancona
Michael Bowman Applied Information Sciences http://www.jsgeo.com/img/speakers/michael_bowman.jpg Dayton, OH 39.758948 -84.191607 1 y
Bryan McBride Fulcrum http://www.jsgeo.com/img/speakers/bryan_mcbride.jpg Niskayuna, NY 42.800005 -73.891441 1 y @brymcbride
Christopher Pollard Delaware Valley Regional Planning http://www.jsgeo.com/img/speakers/christopher_pollard.jpg Philadelphia, PA 39.952584 -75.165222 1 y @CRVanPollard
Chris Whong CartoDB http://www.jsgeo.com/img/speakers/chris_whong.jpg New York, NY 40.712784 -74.005941 1 y @chris_whong
Patricio Gonzalez Vivo Mapzen http://www.jsgeo.com/img/speakers/patricio_gonzalez_vivo.jpg Brooklyn, NY 40.678178 -73.944158 1 y @patriciogv
Anand Thakker Development Seed http://www.jsgeo.com/img/speakers/anand_thakker.jpg Baltimore, MD 39.290385 -76.612189 1 y @anandthakker
Carl Lewis Hackademic http://www.jsgeo.com/img/speakers/carl_lewis.jpg New York, NY 40.712784 -74.005941 1 y @carlvlewis
Diana Shkolnikov Mapzen http://www.jsgeo.com/img/speakers/diana_shkolnikov.png Furlong, PA 40.296218 -75.082115 1 y @dianashk
Erez Cohen Mapsense http://www.jsgeo.com/img/speakers/erez_cohen.jpg San Francisco, CA 37.77493 -122.419416 1 y @erex78
Jill Hubley Indie Developer http://www.jsgeo.com/img/speakers/jill_hubley.jpg Brooklyn, NY 40.678178 -73.944158 1 y @Jill_hubley
Max Galka Metrocosm http://www.jsgeo.com/img/speakers/max_galka.jpg New York, NY 40.712784 -74.005941 1 y @galka_max
Norman Barker IBM MobileFirst http://www.jsgeo.com/img/speakers/norman_barker.jpg Longmont, CO 40.167207 -105.101927 1 y @normanbarker
Andrew Turner Esri http://www.jsgeo.com/img/speakers/andrew_turner.jpg Washington, DC 38.907192 -77.036871 1 y @ajturner
Alex Bostic AECOM http://www.jsgeo.com/img/speakers/alex_bostic.jpg Washington, DC 38.907192 -77.036871 1 y
Nicholas Hallahan SpatialDev http://www.jsgeo.com/img/speakers/nicholas_hallahan.jpg Portland, OR 45.523062 -122.676482 1 y @OgSempervirens
Lou Huang Mapzen http://www.jsgeo.com/img/speakers/lou_huang.jpg New York, NY 40.712784 -74.005941 1 n @saikofish
Drew Bollinger Development Seed http://www.jsgeo.com/img/speakers/drew_bollinger.png Washington, DC 38.907192 -77.036871 1 n @drewbo19
Ishmael Smyrnow AppGeo http://www.jsgeo.com/img/speakers/ishmael_smyrnow.jpg Boston, MA 42.360083 -71.05888 1 n
Calvin Metcalf AppGeo http://www.jsgeo.com/img/speakers/calvin_metcalf.jpg Boston, MA 42.360083 -71.05888 1 y @CWMma
Tom MacWright Mapbox http://www.jsgeo.com/img/speakers/tom_macwright.png Washington, DC 38.907192 -77.036871 1 n @tmcw
Morgan Herlocker Mapbox http://www.jsgeo.com/img/speakers/morgan_herlocker.jpg Washington, DC 38.907192 -77.036871 1 n @morganherlocker
Aaron Petcoff New York Magazine http://www.jsgeo.com/img/speakers/aaron_petcoff.jpg Brooklyn, NY 40.678178 -73.944158 1 n
Stuart Lynn CartoDB http://www.jsgeo.com/img/speakers/stuart_lynn.png New York, NY 40.712784 -74.005941 1 n @Stuart_Lynn
// modified from: http://geoexamples.com/geoexamples/d3js/d3js_electoral_map/tooltipCode.html
d3.helper = {};
d3.helper.tooltip = function(accessor){
return function(selection){
var tooltipDiv;
var bodyNode = d3.select('body').node();
selection.on("mouseover", function(d, i){
var targetCX = parseFloat(
d3.select(this.parentNode).select(".origin").attr("cx"));
var targetCY = parseFloat(
d3.select(this.parentNode).select(".origin").attr("cy"));
// Clean up lost tooltips
d3.select('body').selectAll('div.tooltip').remove();
// Append tooltip
tooltipDiv = d3.select('body').append('div').attr('class', 'tooltip');
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv
.style("left", (targetCX + 20) + "px")
.style('top', (targetCY + 50) +'px')
.style('position', 'absolute')
.style('z-index', 1001);
// Add text using the accessor function
var tooltipText = accessor(d, i) || '';
// Crop text arbitrarily
//tooltipDiv.style('width', function(d, i){return (tooltipText.length > 80) ? '300px' : null;})
// .html(tooltipText);
})
.on('mousemove', function(d, i) {
// Move tooltip
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv
.style('left', d.cx +'px') // 20
.style('top', d.cy +'px');
var tooltipText = accessor(d, i) || '';
tooltipDiv.html(tooltipText);
})
.on("mouseout", function(d, i){
// Remove tooltip
tooltipDiv.remove();
});
};
};
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment