Built with blockbuilder.org
Last active
July 20, 2019 05:59
-
-
Save dianaow/952a337ea558ac2a59f0dda2069fcf08 to your computer and use it in GitHub Desktop.
Marker animation along SVG paths
This file contains hidden or 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
license: mit |
This file contains hidden or 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
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 |
This file contains hidden or 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> | |
<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> |
This file contains hidden or 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
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