Skip to content

Instantly share code, notes, and snippets.

@michael-groble
Last active December 11, 2015 01:49
Show Gist options
  • Save michael-groble/4526452 to your computer and use it in GitHub Desktop.
Save michael-groble/4526452 to your computer and use it in GitHub Desktop.
dynamic trajectories with D3
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);
}
});
}
});
<!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>
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