Skip to content

Instantly share code, notes, and snippets.

@rveciana
Last active August 29, 2015 14:03
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 rveciana/2986e55a01c7008d4b5b to your computer and use it in GitHub Desktop.
Save rveciana/2986e55a01c7008d4b5b to your computer and use it in GitHub Desktop.
D3 Trail Layout animated map

This an example using the D3 trail layout made by Benjamin Schmidt. In this case, the animated Hayian Typhoon track map is shown, with the colour changing with the typhoon class, as in this example, but in an easier and cleaner way.

There is a blog entry explaining this example at GeoExamples

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.map {
fill: none;
stroke: #777;
stroke-opacity: .5;
stroke-width: .5px;
}
.land {
fill: #999;
}
.boundary {
fill: none;
stroke: #fff;
stroke-width: .5px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="http://d3js.org/colorbrewer.v1.min.js"></script>
<script src="pathlayout.js"></script>
<script>
var width = 600,
height = 500;
var projection = d3.geo.mercator()
.scale(5*(width + 1) / 2 / Math.PI)
.translate([width / 2, height / 2])
.rotate([-125, -15, 0])
.precision(.1);
var path = d3.geo.path()
.projection(projection);
d3.json("/mbostock/raw/4090846/world-50m.json", function(error, world) {
d3.json("track.json", function(error, track) {
var color_scale = d3.scale.quantile().domain([1, 5]).range(colorbrewer.YlOrRd[5]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var trail = d3.layout.trail()
.positioner(function(d) {return projection([d.lon,d.lat]);})
.coordType('xy');
var trail_layout = trail.data(track).layout();
svg.insert("path", ".map")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
svg.insert("path", ".map")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
var hayan_trail = svg.selectAll("d").data(trail_layout);
hayan_trail.enter()
.append('line')
.attr("x1",function(d) {return d.x1})
.attr("x2",function(d) {return d.x1})
.attr("y1",function(d) {return d.y1})
.attr("y2",function(d) {return d.y1})
.attr("class","line")
.style("stroke-width",4)
.attr("stroke", function(d){return color_scale(d.class);})
.transition()
.ease("linear")
.delay(function(d,i) {return i*500})
.duration(500)
.attr("x2",function(d) {return d.x2})
.attr("y2",function(d) {return d.y2})
;
});
});
</script>
d3.layout.trail = function() {
var that = {}; //output object
var time = function() {}, //how to access the time data (must be numeric--should but doesn't handle dates);
currentTime, //points of this time will be display with full opacity;
//later points are dropped;
decayRange, //points of this age will have opacity 0. If either currentTime or decayRange is not defined, opacity will be added as some undefined values.
data, // the data being arranged
positioner, // a function that returns the [x,y] for the point.
sort, // a function specifying the sort order
coordType = 'coordinates', //either "coordinates" or "xy"; if the first, returns a "coordinates" array; if the latter, returns x1,y1,x2,y2
grouping; // a function to split the data up into multiple segments;
grouping = function(d) {
return 1
}
positioner = function(datum) {
//given a datum, returns an [x,y] array.
//Might be a projection, for example, or a scale output.
return [datum.x,datum.y]
}
lineToSegments = function(values) {
//the returned array will be filtered to only include segments that fit the defined values.
if (currentTime != undefined & decayRange != undefined) {
values = values.filter(function(d) {
return (time(d) <= currentTime && time(d) >= (currentTime-decayRange))
})
}
values = d3
.nest()
.key(function(d) {return grouping(d)})
.entries(values);
tmp = values;
output = [];
var i = 0
values.forEach(function(element) {
i++;
if (sort!=undefined) {
element.values.sort(sort)
}
if (i==1) {
//console.log(element)
}
var values = element.values;
for (var i = 0; i < (values.length); i++) {
var current = values[i];
if (values[i+1] != undefined) {
current.next = values[i+1]
} else {
current.next = {}
}
if (values[i-1] != undefined) {
current.previous = values[i-1]
if (coordType=="coordinates") {
current.coordinates = [
positioner(values[i-1]),
positioner(values[i])
]
} else if (coordType=="xy") {
var a = positioner(values[i-1]),
b = positioner(values[i]);
current.x1=a[0]
current.y1=a[1]
current.x2=b[0]
current.y2=b[1]
}
current.type = "LineString";
//opacity will probably be this: the percentage of the decay range ago that it was.
//Early tests should guarantee a result between 0 and 1.
}
current.opacity = 1-(currentTime-time(current))/decayRange
}
output = output.concat(values);
})
return output;
}
that.layout = function() {
output = lineToSegments(data);
return output;
}
that.coordType = function(x) {
if (!arguments.length) return coordType;
coordType= x
return that
}
that.grouping = function(x) {
if (!arguments.length) return grouping;
grouping= x
return that
}
that.time = function(x) {
if (!arguments.length) return time;
time = x
return that
}
that.currentTime = function(x) {
if (!arguments.length) return currentTime;
currentTime= x
return that
}
that.decayRange = function(x) {
if (!arguments.length) return decayRange;
decayRange= x
return that
}
that.data = function(x,append) {
if (!arguments.length) return data;
if (append) {
data = data || [];
data = data.concat(x)
} else {
data = x
}
return that
}
that.positioner = function(x) {
if (!arguments.length) return positioner;
positioner = x
return that
}
that.sort = function(x) {
if (!arguments.length) return sort;
sort= x
return that
}
return that;
}
[{"day":3, "hour":18, "lat":6.1, "lon":153.3, "class": 2},{"day":3, "hour":21, "lat":6.1, "lon":152.8, "class": 2},{"day":4, "hour":0, "lat":6.1, "lon":152.2, "class": 3},{"day":4, "hour":3, "lat":6.2, "lon":151.2, "class": 3},{"day":4, "hour":6, "lat":6.2, "lon":150.4, "class": 3},{"day":4, "hour":9, "lat":6.2, "lon":149.5, "class": 3},{"day":4, "hour":12, "lat":6.3, "lon":148.6, "class": 3},{"day":4, "hour":15, "lat":6.3, "lon":148.4, "class": 3},{"day":4, "hour":18, "lat":6.5, "lon":147.6, "class": 3},{"day":4, "hour":21, "lat":6.5, "lon":147.0, "class": 3},{"day":5, "hour":0, "lat":6.5, "lon":145.9, "class": 4},{"day":5, "hour":3, "lat":6.5, "lon":145.2, "class": 4},{"day":5, "hour":6, "lat":6.5, "lon":144.6, "class": 4},{"day":5, "hour":9, "lat":6.5, "lon":144.0, "class": 4},{"day":5, "hour":12, "lat":6.9, "lon":143.1, "class": 4},{"day":5, "hour":15, "lat":7.0, "lon":142.1, "class": 4},{"day":5, "hour":18, "lat":7.1, "lon":141.3, "class": 5},{"day":5, "hour":21, "lat":7.3, "lon":140.5, "class": 5},{"day":6, "hour":0, "lat":7.3, "lon":139.7, "class": 5},{"day":6, "hour":3, "lat":7.5, "lon":138.9, "class": 5},{"day":6, "hour":6, "lat":7.6, "lon":138.0, "class": 5},{"day":6, "hour":9, "lat":7.7, "lon":137.2, "class": 5},{"day":6, "hour":12, "lat":7.9, "lon":136.2, "class": 5},{"day":6, "hour":15, "lat":8.1, "lon":135.3, "class": 5},{"day":6, "hour":18, "lat":8.2, "lon":134.4, "class": 5},{"day":6, "hour":21, "lat":8.4, "lon":133.6, "class": 5},{"day":7, "hour":0, "lat":8.7, "lon":132.8, "class": 5},{"day":7, "hour":3, "lat":9.0, "lon":131.9, "class": 5},{"day":7, "hour":6, "lat":9.3, "lon":131.1, "class": 5},{"day":7, "hour":9, "lat":9.8, "lon":130.2, "class": 5},{"day":7, "hour":12, "lat":10.2, "lon":129.1, "class": 5},{"day":7, "hour":15, "lat":10.4, "lon":128.0, "class": 5},{"day":7, "hour":18, "lat":10.6, "lon":126.9, "class": 5},{"day":7, "hour":21, "lat":10.8, "lon":125.9, "class": 5},{"day":8, "hour":0, "lat":11.0, "lon":124.8, "class": 5},{"day":8, "hour":3, "lat":11.2, "lon":123.7, "class": 5},{"day":8, "hour":6, "lat":11.4, "lon":122.6, "class": 5},{"day":8, "hour":9, "lat":11.5, "lon":121.6, "class": 5},{"day":8, "hour":12, "lat":11.8, "lon":120.7, "class": 5},{"day":8, "hour":15, "lat":12.3, "lon":119.4, "class": 5},{"day":8, "hour":18, "lat":12.4, "lon":118.2, "class": 5},{"day":8, "hour":21, "lat":12.5, "lon":117.3, "class": 5},{"day":9, "hour":0, "lat":12.3, "lon":116.6, "class": 5},{"day":9, "hour":3, "lat":12.9, "lon":115.6, "class": 5},{"day":9, "hour":9, "lat":13.9, "lon":113.9, "class": 5},{"day":9, "hour":12, "lat":14.4, "lon":113.1, "class": 5},{"day":9, "hour":15, "lat":15.0, "lon":112.2, "class": 5},{"day":9, "hour":18, "lat":15.4, "lon":111.4, "class": 5},{"day":9, "hour":21, "lat":15.9, "lon":111.1, "class": 5},{"day":10, "hour":0, "lat":16.5, "lon":110.3, "class": 5},{"day":10, "hour":3, "lat":17.0, "lon":109.7, "class": 5},{"day":10, "hour":6, "lat":17.8, "lon":109.0, "class": 5},{"day":10, "hour":9, "lat":18.5, "lon":108.4, "class": 5},{"day":10, "hour":12, "lat":19.4, "lon":108.1, "class": 5},{"day":10, "hour":15, "lat":19.8, "lon":107.9, "class": 5},{"day":10, "hour":18, "lat":20.3, "lon":107.5, "class": 4},{"day":10, "hour":21, "lat":20.8, "lon":107.1, "class": 4},{"day":11, "hour":0, "lat":21.3, "lon":107.2, "class": 4},{"day":11, "hour":3, "lat":22.0, "lon":107.2, "class": 4},{"day":11, "hour":6, "lat":22.3, "lon":107.4, "class": 3},{"day":11, "hour":9, "lat":22.6, "lon":107.6, "class": 3},{"day":11, "hour":12, "lat":23.0, "lon":107.0, "class": 2}]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment