|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>New York Times map reproduction</title> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
body { |
|
padding: 0; |
|
margin: 0; |
|
font-family: helvetica, arial, sans-serif; |
|
} |
|
|
|
.bold { |
|
font-weight: bold; |
|
} |
|
|
|
.raster { |
|
fill: none; |
|
opacity: 1; |
|
} |
|
|
|
.neutral-district { |
|
fill: #FFF; |
|
opacity: 0.8; |
|
} |
|
.taliban-district { |
|
fill: #C00; |
|
opacity: 0.8; |
|
} |
|
.contested-district { |
|
fill: #FDBF4F; |
|
opacity: 0.8; |
|
} |
|
.neutral-district, |
|
.taliban-district, |
|
.contested-district { |
|
stroke: #6E6E6E; |
|
stroke-opacity: 0.2; |
|
stroke-width: 0.5px; |
|
} |
|
|
|
.provinces { |
|
fill: none; |
|
stroke: #6E6E6E; |
|
stroke-opacity: 0.4; |
|
stroke-width: 0.5px; |
|
} |
|
|
|
.country-border { |
|
fill: none; |
|
stroke: #6E6E6E; |
|
stroke-opacity: 0.7; |
|
stroke-width: 1px; |
|
} |
|
|
|
.city-label { |
|
text-anchor: middle; |
|
margin: 0; |
|
font-size: 15px; |
|
line-height: 14px; |
|
font-weight: 500; |
|
text-align: right; |
|
opacity: 0.6; |
|
color: #000; |
|
} |
|
|
|
.city-marker { |
|
fill: none; |
|
stroke: #000; |
|
opacity: 0.6; |
|
stroke-width: 2px; |
|
} |
|
|
|
.text-note { |
|
font-size: 15px; |
|
font-weight: 500; |
|
color: #000; |
|
opacity: 0.6; |
|
line-height: 18px; |
|
margin: 0; |
|
} |
|
|
|
.legend { |
|
font-size: 15px; |
|
line-height: 24px; |
|
font-weight: 500; |
|
color: #333; |
|
} |
|
|
|
.district-line, |
|
.city-line { |
|
stroke: #000; |
|
stroke-width: 1.2px; |
|
stroke-opacity: 0.5; |
|
opacity: 0.8; |
|
fill: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.country-label { |
|
font-weight: 500; |
|
text-transform: uppercase; |
|
text-anchor: middle; |
|
opacity: 0.4; |
|
color: #000; |
|
font-size: 24px; |
|
line-height: 28px; |
|
letter-spacing: 0.3em; |
|
} |
|
|
|
.distance-scale { |
|
font-size: 11px; |
|
line-height: 11px; |
|
position: absolute; |
|
font-weight: 500; |
|
text-transform: uppercase; |
|
color: #000; |
|
} |
|
|
|
.distance-scale-line { |
|
stroke: #000; |
|
stroke-width: 1; |
|
stroke-opacity: 1; |
|
opacity: 1; |
|
fill: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.byline { |
|
font-weight: 400; |
|
font-size: 12px; |
|
opacity: 0.6; |
|
color: #000; |
|
} |
|
|
|
</style> |
|
</head> |
|
<body> |
|
<svg id="map"></svg> |
|
<div class="byline"> |
|
Thomas Thoren | Source: The New York Times |
|
</div> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script src="//d3js.org/queue.v1.min.js"></script> |
|
<script src="//d3js.org/topojson.v1.min.js"></script> |
|
<script> |
|
|
|
var keeper_cities = [ |
|
'Herat', |
|
'Kandahar', |
|
'Kabul', |
|
'Gardiz', |
|
'Baghlan', |
|
'Kondoz', |
|
'Jalalabad' |
|
]; |
|
|
|
var taliban_districts = [ |
|
'Aliabad', |
|
'Azra', |
|
'Baghran', |
|
'Baharak', |
|
'Bala Buluk', |
|
'Bangi', |
|
'Baraki Barak', |
|
'Chahar Dara', |
|
'Charkh', |
|
'Charsada', |
|
'Dara', |
|
'Dashte Archi', |
|
'Dishu', |
|
'Ghorak', |
|
'Ishkamish', |
|
'Jawand', |
|
'Kakar', |
|
'Khaki Safed', |
|
'Kham Ab', |
|
'Khanabad', |
|
'Kharwar', |
|
'Kohistanat', |
|
'Nawa', |
|
'Pashtun Kot', |
|
'Registan', |
|
'Saydabad', |
|
'Shorabak', |
|
'Tagab', |
|
'Warduj\n', |
|
'Waygal', |
|
'Waza Khwa', |
|
'Yamgan (Girwan)', |
|
'Yangi Qala', |
|
'Zurmat' |
|
]; |
|
|
|
var contested_districts = [ |
|
'Ajristan', |
|
'Argo', |
|
'Baghlani Jadid', |
|
'Barmal', |
|
'Dahana-I- Ghuri', |
|
'Dangam', |
|
'Darayim', |
|
'Dihrawud', |
|
'Dila', |
|
'Ghazni', |
|
'Gizab', |
|
'Gulistan', |
|
'Gurziwan', |
|
'Imam Sahib', |
|
'Jurm', |
|
'Kajaki', |
|
'Khas Uruzgan', |
|
'Khwaja Ghar', |
|
'Kishim', |
|
'Kunduz', |
|
'Marawara', |
|
'Musa Qala', |
|
'Naw Zad', |
|
'Nijrab', |
|
'Nika', |
|
'Qalay-I- Zal', |
|
'Qaysar', |
|
'Sangin', |
|
'Shahidi Hassas', |
|
'Shindand', |
|
'Sozma Qala', |
|
'Tala Wa Barfak', |
|
'Tulak', |
|
'Urgun', |
|
'Yahya Khel', |
|
'Yosuf Khel', |
|
'Ziruk' |
|
]; |
|
|
|
// Make sure at least one dimension is smaller than raster image (874 x 670). |
|
var map_width = 850, |
|
map_height = 700; |
|
|
|
var svg = d3.selectAll("#map") |
|
.attr("width", map_width) |
|
.attr("height", map_height); |
|
|
|
// Create a unit projection |
|
var map_projection = d3.geo.mercator() |
|
.scale(1) |
|
.translate([0, 0]); |
|
|
|
var map_path = d3.geo.path() |
|
.projection(map_projection); |
|
|
|
queue() |
|
.defer(d3.json, "afghanistan-districts.json") |
|
.defer(d3.json, "afghanistan-provinces.json") |
|
.defer(d3.json, "afghanistan-cities.json") |
|
.await(ready); |
|
|
|
function ready(error, districts, provinces, cities) { |
|
if (error) throw error; |
|
|
|
// Scale and center the map to fit into the given dimensions. |
|
var b = map_path.bounds(topojson.feature(districts, districts.objects['afghanistan-districts'])); |
|
|
|
// Pixels per map-path-degree, for both directions: |
|
// TODO: Back to 1 |
|
var s = 0.95 / Math.max((b[1][0] - b[0][0]) / map_width, (b[1][1] - b[0][1]) / map_height); // 0.95 is for padding. 1.0 would fill entire bounding box. |
|
var t = [(map_width - s * (b[1][0] + b[0][0])) / 2, (map_height - s * (b[1][1] + b[0][1])) / 2]; |
|
|
|
// Scale and center vector |
|
map_projection |
|
.scale(s) |
|
.translate(t); |
|
|
|
// Scale and position shaded relief raster image. Assumes already cropped. |
|
var raster_width = (b[1][0] - b[0][0]) * s; |
|
var raster_height = (b[1][1] - b[0][1]) * s; |
|
|
|
var rtranslate_x = (map_width - raster_width) / 2; |
|
var rtranslate_y = (map_height - raster_height) / 2; |
|
|
|
// Shaded relief |
|
svg.append("image") |
|
.attr('id', 'Raster') |
|
.attr("clip-path", "url(#afghanistan_clip)") |
|
.attr("xlink:href", "afghanistan.png") |
|
.attr("class", "raster") |
|
.attr("width", raster_width) |
|
.attr("height", raster_height) |
|
.attr("transform", "translate(" + rtranslate_x + ", " + rtranslate_y + ")"); |
|
|
|
// Draw districts |
|
svg.append("g") |
|
.attr('id', 'Districts') |
|
.selectAll("path") |
|
.data(topojson.feature(districts, districts.objects['afghanistan-districts']).features) |
|
.enter().append("path") |
|
.attr("class", function(d) { |
|
var taliban_condition = taliban_districts.indexOf(d.properties.DIST_34_NA) > -1; |
|
var contested_condition = contested_districts.indexOf(d.properties.DIST_34_NA) > -1; |
|
|
|
if (taliban_condition) { |
|
return "taliban-district"; |
|
} else if (contested_condition) { |
|
return "contested-district"; |
|
} else { |
|
return "neutral-district"; |
|
} |
|
}) |
|
.attr("d", map_path); |
|
|
|
// Draw provinces (made up of districts) |
|
svg.append("g") |
|
.attr('id', 'Provinces') |
|
.selectAll("path") |
|
.data(topojson.feature(provinces, provinces.objects['afghanistan-provinces']).features) |
|
.enter().append("path") |
|
.attr("class", "provinces") |
|
.attr("d", map_path); |
|
|
|
// Draw country border |
|
svg.append("g") |
|
.attr('id', 'CountryBorder') |
|
.datum(topojson.mesh(districts, districts.objects['afghanistan-districts'], function(a, b) { return a === b; })) |
|
.append("path") |
|
.attr("class", "country-border") |
|
.attr("id", "afghanistan_border") // For shaded relief |
|
.attr("d", map_path); |
|
|
|
// Draw city markers |
|
svg.append('g') |
|
.attr('id', 'CityMarkers') |
|
.selectAll("circle") |
|
.data(cities.features) |
|
.enter().append("circle") |
|
.attr("class", "city-marker") |
|
.attr('r', '4px') |
|
.attr("transform", function(d) { return "translate(" + map_projection(d.geometry.coordinates) + ")"; }) |
|
.filter(function(d) { |
|
var condition = ( |
|
keeper_cities.indexOf(d.properties.NAME) === -1 |
|
|| d.properties.NAME === 'Balkh' |
|
); |
|
return condition; |
|
}).remove(); |
|
|
|
// Write city label text |
|
svg.append('g').attr('id', 'CityLabels').selectAll('.city-label') |
|
.data(cities.features) |
|
.enter().append('text') |
|
.attr("class", "city-label") |
|
.each(function(d) { |
|
d3.select(this) |
|
.attr("transform", function(d) { return "translate(" + map_projection(d.geometry.coordinates) + ")"; }) |
|
.attr("dx", "5") |
|
.attr("dy", "15") |
|
.style("text-anchor", "start") |
|
.text(function(d) { return d.properties.NAME; }); |
|
}) |
|
.filter(function(d) { |
|
var condition = ( |
|
keeper_cities.indexOf(d.properties.NAME) === -1 |
|
|| d.properties.NAME === 'Balkh' |
|
); |
|
return condition; |
|
}).remove(); |
|
|
|
// Country label |
|
svg.append("text") |
|
.attr("class", "country-label") |
|
.attr("x", map_width * 0.4) |
|
.attr("y", map_height * 0.5) |
|
.text('Afghanistan'); |
|
|
|
// Draw line between district and text |
|
// Line path generator |
|
var line = d3.svg.line() |
|
.x(function(d) { return d.x; }) |
|
.y(function(d) { return d.y; }) |
|
.interpolate("basis"); |
|
|
|
svg.selectAll(".district-line") |
|
.data(topojson.feature(districts, districts.objects['afghanistan-districts']).features) |
|
.enter().append('path') |
|
.attr("class", "district-line") |
|
.attr("d", function(d) { |
|
var centroid = map_path.centroid(d); |
|
|
|
if (d.properties.DIST_34_NA === 'Waygal') { |
|
var lineData = [ |
|
{"x": centroid[0], "y": centroid[1]}, |
|
{"x": centroid[0] + 70, "y": centroid[1] + 70} |
|
]; |
|
} else if (d.properties.DIST_34_NA === 'Warduj\n') { // Bad data |
|
var lineData = [ |
|
{"x": centroid[0], "y": centroid[1]}, |
|
{"x": centroid[0] + 70, "y": centroid[1] + 70} |
|
]; |
|
} else { |
|
return; |
|
} |
|
return line(lineData); |
|
}) |
|
.filter(function(d) { |
|
return d.properties.DIST_34_NA !== 'Waygal' && d.properties.DIST_34_NA !== 'Warduj\n'; |
|
}).remove(); |
|
// Draw line between city and text |
|
svg.selectAll(".city-line") |
|
.data(cities.features) |
|
.enter().append('path') |
|
.attr("class", "city-line") |
|
.attr("d", function(d) { |
|
var centroid = map_projection(d.geometry.coordinates); |
|
|
|
if (d.properties.NAME === 'Kondoz') { |
|
var lineData = [ |
|
{"x": centroid[0] - 3, "y": centroid[1] - 3}, |
|
{"x": centroid[0] - 70, "y": centroid[1] - 70} |
|
]; |
|
} else { |
|
return; |
|
} |
|
return line(lineData); |
|
}) |
|
.filter(function(d) { |
|
return d.properties.NAME !== 'Kondoz'; |
|
}).remove(); |
|
|
|
// Write districts text note |
|
svg.selectAll('.text-note') |
|
.data(topojson.feature(districts, districts.objects['afghanistan-districts']).features) |
|
.enter().append('text') |
|
.attr("class", "text-note") |
|
.each(function(d) { |
|
if (d.properties.DIST_34_NA === 'Waygal') { |
|
d3.select(this) |
|
.attr("transform", function(d) { return "translate(" + map_path.centroid(d) + ")"; }) |
|
.append("tspan") |
|
.attr("dx", "75") |
|
.attr("dy", "85") |
|
.style("text-anchor", "start") |
|
.text("The Taliban took "); |
|
d3.select(this) |
|
.append("tspan") |
|
.attr("x", "75") |
|
.attr("y", "102") |
|
.style("text-anchor", "start") |
|
.text("control of "); |
|
d3.select(this) |
|
.append("tspan") |
|
.attr("class", "bold") |
|
.text("Waygal"); |
|
d3.select(this) |
|
.append("tspan") |
|
.attr("x", "75") |
|
.attr("y", "119") |
|
.style("text-anchor", "start") |
|
.text("district in June."); |
|
} else if (d.properties.DIST_34_NA === 'Warduj\n') { |
|
d3.select(this) |
|
.attr("transform", function(d) { return "translate(" + map_path.centroid(d) + ")"; }) |
|
.append("tspan") |
|
.attr("dx", "75") |
|
.attr("dy", "85") |
|
.style("text-anchor", "start") |
|
.text("The Taliban overran "); |
|
d3.select(this) |
|
.append("tspan") |
|
.attr("x", "75") |
|
.attr("y", "102") |
|
.style("text-anchor", "start") |
|
.attr("class", "bold") |
|
.text("Wardoj "); |
|
d3.select(this) |
|
.append("tspan") |
|
.text("and "); |
|
d3.select(this) |
|
.append("tspan") |
|
.attr("class", "bold") |
|
.text("Baharak "); |
|
d3.select(this) |
|
.append("tspan") |
|
.attr("x", "75") |
|
.attr("y", "119") |
|
.style("text-anchor", "start") |
|
.text("districts in early "); |
|
d3.select(this) |
|
.append("tspan") |
|
.attr("x", "75") |
|
.attr("y", "136") |
|
.style("text-anchor", "start") |
|
.text("October."); |
|
} else { |
|
return; |
|
} |
|
}) |
|
.filter(function(d) { |
|
return d.properties.DIST_34_NA !== 'Waygal' && d.properties.DIST_34_NA !== 'Warduj\n'; |
|
}).remove(); |
|
|
|
// Write text for cities |
|
svg.selectAll('.text-note') |
|
.data(cities.features) |
|
.enter().append('text') |
|
.attr("class", "text-note") |
|
.each(function(d) { |
|
if (d.properties.NAME === 'Kondoz') { |
|
d3.select(this) |
|
.attr("transform", function(d) { return "translate(" + map_path.centroid(d) + ")"; }) |
|
.append("tspan") |
|
.attr("dx", "-75") |
|
.attr("dy", "-85") |
|
.style("text-anchor", "end") |
|
.text("Taliban fighters took control of "); |
|
d3.select(this) |
|
.append("tspan") |
|
.attr("class", "bold") |
|
.text("Kondoz, "); |
|
d3.select(this) |
|
.append("tspan") |
|
.text("a provincial"); |
|
d3.select(this) |
|
.append("tspan") |
|
.attr("x", "-75") |
|
.attr("y", "-68") |
|
.style("text-anchor", "end") |
|
.text("capital, in September and held it for over two weeks."); |
|
} else { |
|
return; |
|
} |
|
}) |
|
.filter(function(d) { |
|
return d.properties.NAME !== 'Kondoz'; |
|
}).remove(); |
|
|
|
// Color legend key |
|
var legend = svg.selectAll("#legend") |
|
.data([{"color": "#C00", "text": "Taliban-controlled districts"}, {"color": "#FDBF4F", "text": "Contested districts"}]) |
|
.enter().append("g") |
|
.attr("class", "legend") |
|
.attr("width", map_width / 2) |
|
.attr("height", map_height / 2) |
|
.attr("transform", function(d, i) { return "translate(" + map_width * 0.65 + ", " + ((map_height * 0.85) + (i * 20)) + ")"; }) |
|
|
|
legend.append('rect') |
|
.attr("width", 32) |
|
.attr("height", 14) |
|
.attr("fill", function(d) { return d.color; }); |
|
|
|
legend.append('text') |
|
.attr("x", 40) |
|
.attr("y", 0) |
|
.attr("dy", "12") |
|
.attr("fill", "black") |
|
.text(function(d) { return d.text; }); |
|
|
|
// Distance scale |
|
function pixelLength(topojson, miles) { |
|
// Calculates the window pixel length for a given map distance. |
|
|
|
var actual_map_bounds = d3.geo.bounds(topojson); |
|
|
|
var radians = d3.geo.distance(actual_map_bounds[0], actual_map_bounds[1]); |
|
var earth_radius = 3959; // miles |
|
var arc_length = radians * earth_radius; // s = r * theta |
|
|
|
var projected_map_bounds = [ |
|
map_projection(actual_map_bounds[0]), |
|
map_projection(actual_map_bounds[1]) |
|
]; |
|
|
|
var projected_width = projected_map_bounds[1][0] - projected_map_bounds[0][0]; |
|
var projected_height = projected_map_bounds[0][1] - projected_map_bounds[1][1]; |
|
var projected_map_hypotenuse = Math.sqrt( |
|
(Math.pow(projected_width, 2)) + (Math.pow(projected_height, 2)) |
|
); |
|
|
|
var pixels_per_mile = projected_map_hypotenuse / arc_length; |
|
var pixel_distance = pixels_per_mile * miles; |
|
|
|
return pixel_distance; |
|
} |
|
|
|
var pixels_for_hundred_miles = pixelLength(topojson.feature(districts, districts.objects['afghanistan-districts']), 100); |
|
|
|
var distance_scale = svg.selectAll("#distance-scale") |
|
.data([pixels_for_hundred_miles]) |
|
.enter().append("g") |
|
.attr("class", "distance-scale") |
|
.attr("width", function(d) { return d; }); |
|
|
|
distance_scale.append('text') |
|
.attr("x", function(d, i) { return map_width * 0.65; }) |
|
.attr("y", function(d, i) { return (map_height * 0.95) + (i * 20); }) |
|
.text("100 miles"); |
|
|
|
distance_scale.append('path') |
|
.attr("class", "distance-scale-line") |
|
.attr("d", function(d, i) { |
|
var lineData = [ |
|
{"x": map_width * 0.65, "y": (map_height * 0.95) + (i * 20) + 3}, |
|
{"x": map_width * 0.65 + d, "y": (map_height * 0.95) + (i * 20) + 3} |
|
]; |
|
|
|
return line(lineData); |
|
}); |
|
|
|
} |
|
|
|
// Allows iframe on bl.ocks.org. |
|
// d3.select(self.frameElement).style("height", map_height + 25 + "px"); |
|
|
|
</script> |
|
</body> |
|
</html> |