Last active
December 11, 2015 01:49
-
-
Save michael-groble/4526452 to your computer and use it in GitHub Desktop.
dynamic trajectories with D3
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
extend = function(proto, properties) { | |
for (var key in properties) { | |
proto[key] = properties[key]; | |
} | |
return proto; | |
} | |
Object.getPrototypeOf(d3.map()).length = function() { | |
var length = 0; | |
this.forEach(function(key) {++length;}); | |
return length; | |
} | |
var expected_update_msec = 10000; | |
var simulated_update_msec = 2000; | |
var location_cleanup_msec = 1000; | |
var tail_duration_msec = 10000; | |
var Device = function(id) { | |
this.id = id; | |
this.locations = []; | |
}; | |
extend(Device.prototype, { | |
last_location: function() { | |
return this.locations[0]; | |
}, | |
last_segment: function() { | |
var current = this.locations[0]; | |
var previous = this.locations[1] || current; | |
if (!current) return; | |
if (current.time - previous.time > expected_update_msec) { | |
previous = current; // start new segment | |
} | |
return {end: current, start: previous}; | |
}, | |
update_location: function(location) { | |
var previous = this.locations[0]; | |
var current = {time: location.time, x: location.x, y: location.y}; | |
this.locations.unshift(current); | |
this.location_updated_at = new Date().getTime(); | |
} | |
}); | |
var Layer = function(id, prefix, parent) { | |
this.id = id; | |
this.prefix = prefix; | |
this.svg = parent.append("g").attr("id", id); | |
}; | |
extend(Layer.prototype, { | |
dissolve: function(transition) { | |
return transition.ease('linear').style("opacity", 1.e-6); | |
}, | |
dissolve_element: function(element_id) { | |
return this.svg.select("#"+element_id).transition().duration(250).call(this.dissolve); | |
} | |
}); | |
var TailLayer = function(id, prefix, parent, duration) { | |
Layer.call(this, id, prefix, parent); | |
this.duration = duration; | |
this.segments = d3.map(); | |
}; | |
TailLayer.prototype = Object.create(Layer.prototype); | |
extend(TailLayer.prototype, { | |
segment_id: function(device_id, n) { | |
return this.prefix + device_id + '-' + n; | |
}, | |
get_or_create_segments: function(device) { | |
var segments = this.segments.get(device.id); | |
if (!segments) { | |
segments = []; | |
this.segments.set(device.id, segments); | |
} | |
return segments; | |
}, | |
update_segments: function(device) { | |
var segment = device.last_segment(); | |
if (!segment) return; | |
var segments = this.get_or_create_segments(device); | |
var current = {time: segment.end.time, end: [segment.end.x, segment.end.y], start: [segment.start.x, segment.start.y]}; | |
var previous = segments[0]; | |
current.n = previous ? previous.n + 1 : 0; | |
segments.unshift(current); | |
return current; | |
}, | |
add_or_update: function(device) { | |
var segment = this.update_segments(device); | |
if (!segment) return; | |
var element_id = this.segment_id(device.id, segment.n); | |
var start_loc = segment.start.join(","); | |
var end_loc = segment.end.join(","); | |
var start_path = "M" + start_loc + " " + start_loc; | |
var full_path = "M" + start_loc + " " + end_loc; | |
var end_path = "M" + end_loc + " " + end_loc; | |
this.svg.append("path") | |
.attr("id", element_id) | |
.attr("class","tail") | |
.attr("d", start_path) | |
.transition().duration(250) | |
.ease('linear') | |
.attr("d", full_path) | |
.transition().delay(this.duration) | |
.duration(250) | |
.ease('linear') | |
.attr("d", end_path) | |
.remove(); | |
}, | |
remove: function(device_id) { | |
var segments = this.segments.get(device_id); | |
if (segments) { | |
var me = this; | |
segments.forEach(function(segment){ | |
var element_id = me.segment_id(device_id, segment.n); | |
me.dissolve_element(element_id).remove(); | |
}); | |
this.segments.remove(device_id); | |
} | |
} | |
}); | |
var PointLayer = function(id, prefix, parent) { | |
Layer.call(this, id, prefix, parent); | |
}; | |
PointLayer.prototype = Object.create(Layer.prototype); | |
extend(PointLayer.prototype, { | |
element_id: function(device_id) { | |
return this.prefix + device_id; | |
}, | |
add_or_update: function(device) { | |
var location = device.last_location(); | |
if (!location) return; | |
var element_id = this.element_id(device.id); | |
var element = this.svg.select("#"+element_id); | |
if (element.empty()) { | |
element = this.svg.append("circle") | |
.attr("id", element_id) | |
.attr("class","device") | |
.attr("cx", location.x) | |
.attr("cy", location.y) | |
.attr("r", 2); | |
} | |
else { | |
element.transition().duration(250) | |
.ease('linear') | |
.attr('cx', location.x) | |
.attr('cy', location.y); | |
} | |
}, | |
remove: function(device_id) { | |
this.dissolve_element(this.element_id(device_id)).remove(); | |
} | |
}); | |
var Controller = function(parent, id, width, height) { | |
this.devices = d3.map(); | |
this.located_device_ids = d3.map(); | |
this.svg = d3.select(parent).append("svg").attr("width", width).attr("height", height); | |
this.map_layers = this.svg.append("g").attr("id",id); | |
this.tails = new TailLayer("tails", "tail_", this.map_layers, tail_duration_msec); | |
this.points = new PointLayer("points", "pnt_", this.map_layers); | |
var me = this; | |
var cleanup = function() { | |
me.remove_old_locations(); | |
window.setTimeout(cleanup, location_cleanup_msec); | |
}; | |
cleanup(); | |
}; | |
extend(Controller.prototype, { | |
update_location: function(location) { | |
var id = location.id; | |
var d = this.devices.get(id); | |
if (!d) { | |
d = new Device(id); | |
this.devices.set(id, d); | |
} | |
this.located_device_ids.set(id, null); | |
d.update_location(location); | |
this.points.add_or_update(d); | |
this.tails.add_or_update(d); | |
}, | |
remove: function(id) { | |
this.located_device_ids.remove(id); | |
this.tails.remove(id); | |
this.points.remove(id); | |
}, | |
remove_old_locations: function() { | |
var now = new Date().getTime(); | |
var drop_time = now - expected_update_msec; | |
var me = this; | |
this.located_device_ids.forEach(function(id) { | |
var device = me.devices.get(id); | |
if (!device || device.location_updated_at < drop_time) { | |
me.located_device_ids.remove(id); | |
me.tails.remove(id); | |
me.points.remove(id); | |
} | |
}); | |
} | |
}); |
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="apple-mobile-web-app-capable" content="yes"/> | |
<style> | |
svg .device { | |
fill: black; | |
} | |
svg .tail { | |
stroke: rgb(180,180,240); | |
stroke-width: 1.5; | |
fill: none; | |
} | |
</style> | |
<script src="http://d3js.org/d3.v2.min.js"></script> | |
<script type="text/javascript" src="dynamic.js"></script> | |
<script type="text/javascript" src="simulator.js"></script> | |
<script type="text/javascript"> | |
document.addEventListener('DOMContentLoaded', function() { | |
var width = 960; | |
var height = 500; | |
var controller = new Controller("#map", "layers", width, height); | |
simulate(controller, width, height); | |
}); | |
</script> | |
</head> | |
<body> | |
<div id="map"></div> | |
</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 simulate = function(controller, width, height) { | |
var sim_devices = []; | |
var num_sims = 50; | |
var s = 0; | |
var base_time = new Date().getTime(); | |
var new_sim = function(id, time) { | |
return { | |
id: id, | |
time: time, | |
x: width * Math.random(), | |
y: height * Math.random(), | |
dx: 8 - 16 * Math.random(), | |
dy: 8 - 16 * Math.random() | |
}; | |
}; | |
var update = function() { | |
var now = new Date().getTime(); | |
var i = s % num_sims; | |
var sim = sim_devices[i]; | |
var delta_sec = 0.001 * (now - sim.time); | |
s += 1; | |
sim.dx += 3 - 6 * Math.random(); | |
sim.dy += 3 - 6 * Math.random(); | |
sim.x += delta_sec * sim.dx; | |
sim.y += delta_sec * sim.dy; | |
sim.time = now; | |
if (sim.x < 2 || sim.x > width - 2 || sim.y < 2 || sim.y > height - 2) { | |
sim = new_sim(s,now); | |
sim_devices[i] = sim; | |
} | |
controller.update_location(sim); | |
window.setTimeout(update, simulated_update_msec / num_sims); | |
}; | |
for (var i = 0; i < num_sims; ++i) { | |
var sim = new_sim(s,base_time); | |
sim_devices[i] = sim; | |
controller.update_location(sim); | |
s += 1; | |
} | |
update(); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment