Skip to content

Instantly share code, notes, and snippets.

@dianaow
Last active July 20, 2019 05:59
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 dianaow/952a337ea558ac2a59f0dda2069fcf08 to your computer and use it in GitHub Desktop.
Save dianaow/952a337ea558ac2a59f0dda2069fcf08 to your computer and use it in GitHub Desktop.
Marker animation along SVG paths
license: mit
country count pct
Afghanistan 439 6.795665634674923
Albania 3 0.04643962848297214
Algeria 10 0.15479876160990713
Andorra 3 0.04643962848297214
Angola 2 0.030959752321981424
Armenia 4 0.06191950464396285
Australia 1 0.015479876160990712
Austria 3 0.04643962848297214
Azerbaijan 5 0.07739938080495357
Bahrain 2 0.030959752321981424
Bangladesh 73 1.130030959752322
Benin 1 0.015479876160990712
Bulgaria 2 0.030959752321981424
Burundi 28 0.43343653250773995
Cambodia 1 0.015479876160990712
Cameroon 14 0.21671826625386997
Canada 1 0.015479876160990712
Central African Republic 3 0.04643962848297214
Chad 3 0.04643962848297214
China 4 0.06191950464396285
Colombia 2 0.030959752321981424
Congo 1 0.015479876160990712
Congo (Democratic Republic of the) 92 1.4241486068111455
Costa Rica 1 0.015479876160990712
Cuba 1 0.015479876160990712
Côte d'Ivoire 9 0.1393188854489164
Djibouti 2 0.030959752321981424
Egypt 39 0.6037151702786377
El Salvador 2 0.030959752321981424
Eritrea 75 1.1609907120743035
Ethiopia 84 1.3003095975232197
Finland 2 0.030959752321981424
France 13 0.20123839009287925
Gambia 31 0.47987616099071206
Georgia 1 0.015479876160990712
Germany 112 1.7337461300309598
Ghana 12 0.18575851393188855
Greece 1 0.015479876160990712
Guinea 6 0.09287925696594428
Haiti 1 0.015479876160990712
India 4 0.06191950464396285
Indonesia 3 0.04643962848297214
Iran (Islamic Republic of) 165 2.5541795665634677
Iraq 166 2.569659442724458
Ireland 1 0.015479876160990712
Italy 2 0.030959752321981424
Jordan 227 3.513931888544892
Kazakhstan 1 0.015479876160990712
Kenya 21 0.32507739938080493
Kyrgyzstan 1 0.015479876160990712
Lebanon 24 0.3715170278637771
Liberia 5 0.07739938080495357
Libya 17 0.2631578947368421
Malawi 1 0.015479876160990712
Malaysia 3 0.04643962848297214
Mali 1 0.015479876160990712
Mauritania 2 0.030959752321981424
Mexico 1 0.015479876160990712
Morocco 11 0.17027863777089783
Myanmar 15 0.23219814241486067
Namibia 1 0.015479876160990712
Nepal 1 0.015479876160990712
Netherlands 3 0.04643962848297214
Niger 1 0.015479876160990712
Nigeria 72 1.1145510835913313
Pakistan 115 1.7801857585139318
Palau 1 0.015479876160990712
Palestine, State of 184 2.848297213622291
Peru 1 0.015479876160990712
Philippines 2 0.030959752321981424
Poland 1 0.015479876160990712
Portugal 1 0.015479876160990712
Russian Federation 9 0.1393188854489164
Rwanda 9 0.1393188854489164
Saudi Arabia 5 0.07739938080495357
Senegal 4 0.06191950464396285
Sierra Leone 12 0.18575851393188855
Somalia 278 4.303405572755418
South Africa 6 0.09287925696594428
South Sudan 28 0.43343653250773995
Sri Lanka 7 0.10835913312693499
Sudan 152 2.3529411764705883
Suriname 1 0.015479876160990712
Sweden 1 0.015479876160990712
Switzerland 4 0.06191950464396285
Syrian Arab Republic 3156 48.85448916408669
Tajikistan 2 0.030959752321981424
Tanzania United Republic of 1 0.015479876160990712
Thailand 3 0.04643962848297214
Togo 3 0.04643962848297214
Trinidad and Tobago 1 0.015479876160990712
Tunisia 17 0.2631578947368421
Turkey 474 7.337461300309598
Turkmenistan 2 0.030959752321981424
Uganda 19 0.29411764705882354
Ukraine 8 0.1238390092879257
United Arab Emirates 3 0.04643962848297214
United Kingdom of Great Britain and Northern Ireland 1 0.015479876160990712
United States of America 1 0.015479876160990712
Venezuela (Bolivarian Republic of) 1 0.015479876160990712
Western Sahara 5 0.07739938080495357
Yemen 83 1.284829721362229
Zambia 1 0.015479876160990712
Zimbabwe 20 0.30959752321981426
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<style>
body {
background-color: #D9D9D9;
}
#wrapper {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
}
#container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#group {
position: relative;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="wrapper">
<div id="container">
<div id='group'>
<div id="chart">
<svg></svg>
</div>
</div>
</div>
</div>
<script src="map-1.js"></script>
<script>
main.run()
</script>
</body>
</html>
var main = function () {
///////////////////////////////////////////////////////////////////////////
///////////////////////////////// Globals /////////////////////////////////
///////////////////////////////////////////////////////////////////////////
var connData, arcs
var canvasDim = { width: screen.width*0.96, height: screen.height}
var margin = {top: 0, right: 0, bottom: 0, left: 0}
var width = canvasDim.width - margin.left - margin.right
var height = canvasDim.height - margin.top - margin.bottom
var chart = d3.select("#chart")
var centroids = []
var DEFAULT_MAP_COLOR = '#7F7F7F'
var DEFAULT_MAP_STROKE = '#D9D9D9'
var DEFAULT_SELECTED_CTRY = '#7F7F7F'
var DEFAULT_PATH_WIDTH = 0.8
var colorSource = '#45ADA8'
var colorDestination = '#FABF4B'
var lineScale = d3.scaleSqrt()
.range([0.3, 30])
.domain([0, 100])
///////////////////////////////////////////////////////////////////////////
///////////////////////////////// Initialize //////////////////////////////
///////////////////////////////////////////////////////////////////////////
return {
run : function () {
//////////////////// Set up and initiate containers ///////////////////////
var svg = chart.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var g = svg.append("g")
.attr('id', 'zoom-group')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
const defs = g.append('defs')
map = g
.append("g")
.attr("id", "map")
arcs = g.append("g")
.attr("class","arcs")
markersGroup = g.append("g")
.attr("class", "markers-group")
loadData()
///////////////////////////////////////////////////////////////////////////
////////////////////////////// Generate data //////////////////////////////
///////////////////////////////////////////////////////////////////////////
function loadData() {
d3.queue() // queue function loads all external data files asynchronously
.defer(d3.json,'https://raw.githubusercontent.com/andybarefoot/andybarefoot-www/master/maps/mapdata/custom50.json')
.defer(d3.csv, 'country_stats.csv')
.await(processData);
}
function processData(error, geoJSON, csv) {
if (error) throw error;
connData = csv // density of connections from refugee country to Germany
var cols = []
world = geoJSON.features; // store the path in variable for ease
for (var i in connData) { // for each geometry object
for (var j in world) { // for each row in the CSV
if (world[j].properties.name == connData[i]['country']) { // if they match
for (var k in connData[i]) { // for each column in the a row within the CSV
if ((k != 'country')) { // let's not add the name or id as props since we already have them
world[j].properties[k] = (connData[i][k] != null ? Number(connData[i][k]) : 0) // add each CSV column key/value to geometry object
}
}
break; // stop looking through the CSV since we made our match
}
}
}
drawMap(world)
updateMap('pct')
arcData = drawLinksMap(world, 'pct')
var connectors = []
var conn_ids = []
arcData.map((d,i)=>{
var p = arc(d, 'sourceLocation', 'targetLocation', 2)
if(p){
if(p.angle>=1 & p.angle<=180) {
var stops = [
{offset: '0%', color: d.startColor, opacity: 1 },
{offset: '100%', color: d.stopColor, opacity: 1 }
]
} else {
var stops = [
{offset: '0%', color: d.stopColor, opacity: 1 },
{offset: '100%', color: d.startColor, opacity: 1 }
]
}
p.stops = stops
p.value = d.value
p.id = i
connectors.push(p)
}
})
//console.log(connectors)
// Create a path for each source/target pair.
var arcPaths = arcs.selectAll("path").data(connectors)
arcPaths.exit().remove()
var entered_arcs = arcPaths.enter().append("path")
.attr('class', 'line')
.attr('id', (d,i)=>'line-'+i)
.attr('d', function(d) { return d.path })
.attr('fill', 'none')
.attr('stroke-width', function(d) { return lineScale(d.value) })
.attr('opacity', 1)
.style("stroke", function(d,i) {
const gradientID = `gradient${i}` // make unique gradient ids
const linearGradient = defs.append('linearGradient')
.attr('id', gradientID)
.attr("gradientTransform", "rotate(90)");
linearGradient.selectAll('stop')
.data(d.stops)
.enter().append('stop')
.attr('offset', l => l.offset)
.attr('stop-color', l => l.color)
.attr('stop-opacity', l => l.opacity)
return `url(#${gradientID})`;
})
arcPaths = arcPaths.merge(entered_arcs)
arcPaths.attr('d', function(d) { return d.path })
}
///////////////////////////////////////////////////////////////////////////
//////////////////////////////// Render map ///////////////////////////////
///////////////////////////////////////////////////////////////////////////
var projection = d3.geoEckert3()
.center([0, 0]) // set centre to further North
.scale([screen.width>1500 ? width/4.5 : width/3.5]) // scale to fit group width
.translate([width/2-80,height/2])
var path = d3.geoPath()
.projection(projection)
function drawMap(data) {
// draw a path for each feature/country
countriesPaths = map
.selectAll("path")
.data(data)
.enter().append("path")
.attr("d", path)
.attr("id", function(d, i) { return "country" + d.properties.name })
.attr("class", "country")
.attr('fill', DEFAULT_MAP_COLOR)
.attr('stroke', DEFAULT_MAP_STROKE)
.attr('stroke-width', '0.4px')
// store an array of each country's centroid
data.map(d=> {
centroids.push({
name: d.properties.name,
x: path.centroid(d)[0],
y: path.centroid(d)[1]
})
})
}
///////////////////////////////////////////////////////////////////////////
//////////////////////////////// Zoomable map /////////////////////////////
///////////////////////////////////////////////////////////////////////////
const mapWidth = svg.node().getBoundingClientRect().width;
const mapHeight = svg.node().getBoundingClientRect().height;
const zoom = d3.zoom()
.scaleExtent([0.7, 1.9])
.translateExtent([[-mapWidth, -mapHeight], [mapWidth, mapHeight]])
.extent([[0,0], [mapWidth, mapHeight]])
.on("zoom", zoomed)
function zoomed(d){
const {x,y,k} = d3.event.transform
let t = d3.zoomIdentity
t = t.translate(x,y).scale(k).translate(50,50)
g.attr("transform", t)
}
svg.call(zoom)
////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////// Chloropleth map ///////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
function updateMap(X) {
countriesPaths
.attr('fill', function(d) {
if ((d.properties[X] === undefined) | (d.properties[X] == 0)) {
return DEFAULT_MAP_COLOR
} else {
return DEFAULT_SELECTED_CTRY
}})
}
////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////// Draw connector paths on map ////////////////////////
////////////////////////////////////////////////////////////////////////////////////
function drawLinksMap(data, X) {
// Create an array to feed into path selection
//var arcdata = [
//{
//sourceName: Singapore,
//targetName: Australia,
//sourceLocation: [-99.5606025, 41.068178502813595],
//targetLocation: [-106.503961875, 33.051502817366334]
//}]
var arcData = []
var country = 'Germany'
data.map((d,i)=>{
if((d.properties[X] !== undefined) & (d.properties[X] !== 0) ) {
var cS = centroids.find(c => c.name == d.properties.name)
var cT = centroids.find(c => c.name == country)
arcOne = {
id: i,
value: d.properties[X],
sourceName: d.properties.name,
targetName: country,
sourceLocation: [cS.x, cS.y],
targetLocation: [cT.x, cT.y],
startColor: colorSource,
stopColor: colorDestination
}
arcData.push(arcOne)
}
})
//console.log(arcData)
return arcData
}
function updateMarkers(elapsed) {
const xProgressAccessor = d => (elapsed - d.startTime) / 5000
if (people.length < 100) {
people = [
...people,
...d3.range(1).map(() => generatePerson(elapsed)),
]
}
const m1 = markersGroup.selectAll(".marker-circle")
.data(people.filter(function(d){
return xProgressAccessor(d) > 0 && xProgressAccessor(d) < 1
}), d => d.id)
m1.enter().append("circle")
.attr("class", "marker marker-circle")
.attr('id', d=>'marker-'+d.id)
.attr("r", 2)
.attr("fill", '#113893')
m1.exit().remove()
const markers = d3.selectAll(".marker")
markers.style("transform", (d,i) => {
var xScale = d3.scaleLinear()
.domain([0, 1])
.range([0, d.path.getTotalLength()])
.clamp(true)
var currentPos = d.path.getPointAtLength(xScale(xProgressAccessor(d)))
return `translate(${ currentPos.x }px, ${ currentPos.y }px)`
})
//if (elapsed > 20000) timer.stop();
}
let people = []
let currentPersonId = 0
function generatePerson(elapsed) {
const getRandomNumberInRange = (min, max) => Math.random() * (max - min) + min
const getRandomValue = arr => arr[Math.floor(getRandomNumberInRange(0, arr.length))]
arcIDs = []
arcs.selectAll("path").each(d=>arcIDs.push(d.id))
const id = getRandomValue(arcIDs)
currentPersonId++
return {
id: currentPersonId,
path: arcs.selectAll("path").filter(d=>d.id == id).node(),
startTime: elapsed + getRandomNumberInRange(-300, 300),
}
}
timer = d3.interval(updateMarkers, 200)
}
}
///////////////////////////////////////////////////////////////////////////
///////////////////////////// Helper functions ////////////////////////////
///////////////////////////////////////////////////////////////////////////
function findCenters(r, p1, p2) {
var pm = { x : 0.5 * (p1.x + p2.x) , y: 0.5*(p1.y+p2.y) } ;
var perpABdx= - ( p2.y - p1.y );
var perpABdy = p2.x - p1.x;
var norm = Math.sqrt(sq(perpABdx) + sq(perpABdy));
perpABdx/=norm;
perpABdy/=norm;
var dpmp1 = Math.sqrt(sq(pm.x-p1.x) + sq(pm.y-p1.y));
var sin = dpmp1 / r ;
if (sin<-1 || sin >1) return null;
var cos = Math.sqrt(1-sq(sin));
var d = r*cos;
var res1 = { x : pm.x + perpABdx*d, y: pm.y + perpABdy*d };
var res2 = { x : pm.x - perpABdx*d, y: pm.y - perpABdy*d };
return { c1 : res1, c2 : res2} ;
}
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle, NUM){
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
if (NUM == 1) {
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, arcSweep, 0, end.x, end.y
].join(" ");
} else {
var d = [
"M", end.x, end.y,
"A", radius, radius, 0, arcSweep, 0, start.x, start.y
].join(" ");
}
var path_vars = {path: d, angle: endAngle}
return path_vars
}
function sq(x) { return x*x ; }
function drawCircleArcSVG(c, r, p1, p2, NUM) {
if(c.x & c.y){
var ang1 = Math.atan2(p1.y-c.y, p1.x-c.x)*180/Math.PI+90;
var ang2 = Math.atan2(p2.y-c.y, p2.x-c.x)*180/Math.PI+90;
var path_vars = describeArc(c.x, c.y, r, ang1, ang2, NUM)
}
return path_vars
}
function line(d, sourceName, targetName){
var sourceLngLat = d[sourceName],
targetLngLat = d[targetName];
if (targetLngLat && sourceLngLat) {
var sourceX = sourceLngLat[0],
sourceY = sourceLngLat[1];
var targetX = targetLngLat[0],
targetY = targetLngLat[1];
var path = [
"M", sourceX, sourceY,
"L", targetX, targetY
].join(" ")
var path_vars = {path: path, angle: 0}
return path_vars
} else {
return "M0,0,l0,0z";
}
}
function arc(d, sourceName, targetName, NUM) {
var sourceLngLat = d[sourceName],
targetLngLat = d[targetName];
if (targetLngLat && sourceLngLat) {
var sourceX = sourceLngLat[0],
sourceY = sourceLngLat[1];
var targetX = targetLngLat[0],
targetY = targetLngLat[1];
var dx = targetX - sourceX,
dy = targetY - sourceY
var initialPoint = { x: sourceX, y: sourceY}
var finalPoint = { x: targetX, y: targetY}
d.r = Math.sqrt(sq(dx) + sq(dy)) * 2;
var centers = findCenters(d.r, initialPoint, finalPoint);
var path_vars = drawCircleArcSVG(centers.c1, d.r, initialPoint, finalPoint, d.category, NUM);
return path_vars
}
}
}()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment