Using d3.geo.tile to display raster image tiles underneath some TopoJSON vectors, and d3.behavior.zoom for pan & zoom. This version adjusts the transform and stroke-width to update the displayed vector data efficiently. You can instead reproject interactively, but changing the transform is typically much faster.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
path {
fill: none;
stroke: red;
stroke-linejoin: round;
stroke-width: 1.5px;
}
</style>
<body>
<div id="controlbar">
<button onclick="colorBy('e')" >Expense</button>
<button onclick="colorBy('s')" >Speed</button>
<button onclick="colorByType()" >Type</button>
</div>
<div id="vizcontainer">
</div>
<footer>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/d3.geo.tile.v0.min.js"></script>
<script src="//d3js.org/topojson.v0.min.js"></script>
<script>
var typeHash = {road: "brown", overseas: "green", coastal: "lightgreen", upstream: "blue", downstream: "blue", ferry: "purple"}
var width = Math.max(960),
height = Math.max(500);
var tile = d3.geo.tile()
.size([width, height]);
var projection = d3.geo.mercator()
.scale((1 << 12) / 2 / Math.PI)
.translate([width / 2, height / 2]);
var projection2 = d3.geo.mercator()
.scale((1 << 11) / 2 / Math.PI)
.translate([width / 2, height / 2]);
var center = projection([12, 42]);
var path = d3.geo.path()
.projection(projection);
var zoom = d3.behavior.zoom()
.scale(projection.scale() * 2 * Math.PI)
.scaleExtent([1 << 12, 1 << 17])
.translate([width - center[0], height - center[1]])
.on("zoom", zoomed);
// With the center computed, now adjust the projection such that
// it uses the zoom behavior’s translate and scale.
projection
.scale(1 / 2 / Math.PI)
.translate([0, 0]);
var svg = d3.select("#vizcontainer").append("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("path");
colorRamp=d3.scale.linear().domain([0,1,5,10]).range(["green", "yellow","orange","red"])
d3.json("july_topo.json", function(error, routes) {
exposedroutes = routes;
svg.call(zoom);
/* vector
.attr("d", path(topojson.mesh(routes, routes.objects.july)))
.style("stroke", "lightgray");
*/
svg.selectAll(".routes")
.data(topojson.object(routes, routes.objects.july2).geometries)
.enter()
.append("path")
.attr("class", "routes")
.attr("d", path)
.style("stroke-width", 2)
.style("stroke-opacity", .75)
.style("stroke", function(d,i) {return colorRamp(d.properties.e)})
.on("mouseover", function(d) {
d3.select(this).transition().duration(500).style("stroke-opacity", 1);
})
.on("mouseout", function() {
d3.select(this).transition().duration(500).style("stroke-opacity", .5);
});;
d3.csv("osites.csv", function(error, sites) {
exposedsites = sites;
var sitesG = svg.append("g").attr("id","sitesG")
.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
var osites = sitesG.selectAll(".sites")
.data(sites)
.enter()
.append("g")
.attr("transform", function(d) {return "translate(" + projection([d.x,d.y]) + ")scale(" + projection.scale() + ")"})
osites
.append("circle")
.attr("r", 15 / zoom.scale())
.attr("class", "sitecirc")
})
zoomed();
});
function zoomed() {
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())
();
// projection.scale(zoom.scale())
// .translate(zoom.translate())
vector
.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")")
.style("stroke-width", 1 / zoom.scale());
d3.selectAll(".routes")
.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")")
.style("stroke-width", 2 / zoom.scale());
// d3.selectAll(".sites")
// .attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")")
// .attr("transform", function(d) {return "translate(" + projection(d.coordinates) + ")scale("+projection.scale()+")"})
d3.select("#sitesG")
.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
d3.selectAll(".sitecirc")
.attr("r", 15 / zoom.scale());
var image = raster
.attr("transform", "scale(" + tiles.scale + ")translate(" + tiles.translate + ")")
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit()
.remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "//" + ["a", "b", "c", "d"][Math.random() * 4 | 0] + ".tiles.mapbox.com/v3/elijahmeeks.map-zm593ocx/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("width", 1)
.attr("height", 1)
.attr("x", function(d) { return d[0]; })
.attr("y", function(d) { return d[1]; });
}
function colorBy(attribute) {
d3.selectAll(".routes")
.transition().duration(500).style("stroke", function(d) {return colorRamp(d.properties[attribute])})
}
function colorByType() {
d3.selectAll(".routes")
.transition().duration(500).style("stroke", function(d) {return typeHash[d.properties.t]})
}
</script>
</footer>
</body>