|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> |
|
<script src="https://unpkg.com/topojson@3"></script> |
|
<style> |
|
body { |
|
margin: 0; |
|
position: fixed; |
|
top: 0; |
|
right: 0; |
|
bottom: 0; |
|
left: 0; |
|
} |
|
|
|
.constituency { |
|
stroke: #ddd; */ |
|
stroke-width: .75px; |
|
} |
|
|
|
.london-outline { |
|
fill: none; |
|
stroke: #333; */ |
|
stroke-width: .75px; |
|
} |
|
|
|
.perception-layer rect { |
|
fill: white; |
|
stroke: red; |
|
pointer-events: all; |
|
} |
|
|
|
.record-text, .timer-text, .coordinates-text { |
|
font-family: sans-serif; |
|
font-weight: bold; |
|
fill: red; |
|
} |
|
|
|
.record-circle { |
|
fill: red; |
|
} |
|
|
|
.info-text { |
|
font-family: sans-serif; |
|
font-weight: bold; |
|
fill: red; |
|
} |
|
|
|
.task-text { |
|
font-size: 24px; |
|
} |
|
|
|
.click-text { |
|
font-size: 18px; |
|
} |
|
|
|
.marker line { |
|
stroke: black; |
|
stroke-width: 2px; |
|
} |
|
|
|
.compare-circle { |
|
fill: white; |
|
fill-opacity: 0; |
|
stroke: black; |
|
stroke-width: 3px; |
|
} |
|
|
|
.compare-text { |
|
font-family: sans-serif; |
|
font-weight: bold; |
|
font-size: 20px; |
|
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff; |
|
} |
|
|
|
</style> |
|
</head> |
|
|
|
<body> |
|
<script> |
|
var margin = {top: 50, right: 50, bottom: 50, left: 50}; |
|
|
|
var width = 960 - margin.left - margin.right, |
|
height = 500 - margin.top - margin.bottom; |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("class", "top-group") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
var colour = d3.scaleSequential(d3.interpolateGreens); |
|
|
|
var mapGroup = svg.append("g") |
|
.attr("class", "map"); |
|
|
|
d3.json("topo_wpc_london.json", (error, map) => { |
|
if (error) throw error; |
|
|
|
var constituencies = topojson.feature(map, map.objects.wpc).features; |
|
var londonOutline = topojson.merge(map, map.objects.wpc.geometries); |
|
|
|
var projection = d3.geoAlbers() |
|
.rotate(0) |
|
.fitSize([width, height], londonOutline); |
|
|
|
var path = d3.geoPath() |
|
.projection(projection); |
|
|
|
var areas = mapGroup.append("g"); |
|
areas.selectAll("path") |
|
.data(constituencies) |
|
.enter().append("path") |
|
.attr("class", "constituency") |
|
.attr("id", d => d.id) |
|
.attr("d", path) |
|
.attr("fill", () => colour(Math.random())); |
|
|
|
var outline = mapGroup.append("g") |
|
.append("path") |
|
.datum(londonOutline) |
|
.attr("class", "london-outline") |
|
.attr("d", path); |
|
|
|
var config = { |
|
task: "Which of the circled areas is darker?", |
|
selectionAccessor: "id", |
|
compareArray: [{id:"E14000615", x:0, y:0, label:"B"}, {id:"E14000732", x:0, y:0, label:"A"}, {id: "E14000687", x:0, y:0, label:"C"}], |
|
compareAccessor: d => { |
|
return projection(d3.polygonCentroid(d3.select("#" + d.id).data()[0].geometry.coordinates[0])) |
|
}, |
|
compareRadius: 30 |
|
} |
|
|
|
appendPerceptionLayer(mapGroup, areas.selectAll("path"), config); |
|
}); |
|
|
|
function appendPerceptionLayer(group, selection, config) { |
|
var newWidth = width + margin.left / 2 + margin.right / 2, |
|
newHeight = height + margin.top / 2 + margin.bottom / 2; |
|
|
|
var perception = group.append("g") |
|
.attr("class", "perception-layer") |
|
.attr("transform", "translate(" + [-margin.left / 2, -margin.top / 2] + ")") |
|
|
|
var outlineRect = perception.append("rect") |
|
.attr("width", newWidth) |
|
.attr("height", newHeight); |
|
|
|
var info = perception.append("g") |
|
.attr("class", "info-text") |
|
.attr("text-anchor", "middle") |
|
.attr("transform", "translate(" + [newWidth / 2, newHeight / 2] + ")"); |
|
info.append("text") |
|
.attr("class", "task-text") |
|
.attr("y", -15) |
|
.text('"' + config.task + '"'); |
|
info.append("text") |
|
.attr("class", "click-text") |
|
.attr("y", 15) |
|
.text("Click to stark the task"); |
|
|
|
var timer = perception.append("text") |
|
.attr("class", "timer-text") |
|
.attr("transform", "translate(" + [newWidth, newHeight] + ")") |
|
.attr("x", -15) |
|
.attr("y", -15) |
|
.attr("text-anchor", "end") |
|
.text("0s"); |
|
|
|
var coordinates = perception.append("text") |
|
.attr("class", "coordinates-text") |
|
.attr("transform", "translate(" + [0, newHeight] + ")") |
|
.attr("x", 15) |
|
.attr("y", -15) |
|
.attr("text-anchor", "start") |
|
.text(""); |
|
|
|
/* Extract this into a separate function or as a path string */ |
|
var markerLength = 10; |
|
var marker = perception.append("g") |
|
.attr("class", "marker") |
|
.attr("transform", "translate(" + [newWidth / 2, newHeight / 2] + ")") |
|
.attr("opacity", 0); |
|
marker.append("line") |
|
.attr("x1", -markerLength).attr("y1", -markerLength) |
|
.attr("x2", markerLength).attr("y2", markerLength); |
|
marker.append("line") |
|
.attr("x1", -markerLength).attr("y1", markerLength) |
|
.attr("x2", markerLength).attr("y2", -markerLength); |
|
|
|
var recordGroup = perception.append("g") |
|
.attr("transform", "translate(" + [-60 + newWidth, 30] + ")"); |
|
|
|
var radius = 6; |
|
|
|
recordGroup.append("text") |
|
.attr("class", "record-text") |
|
.attr("text-anchor", "start") |
|
.attr("x", radius * 1.5) |
|
.text("REC"); |
|
|
|
recordGroup.append("circle") |
|
.attr("class", "record-circle") |
|
.attr("cy", -radius) |
|
.attr("r", radius); |
|
|
|
// If a selection has been provided, bind a click event to this with a given accessor |
|
if (selection) { |
|
selection.on("click", d => { |
|
console.log(d[config.selectionAccessor]); |
|
stopTimer(perception.node()); |
|
}); |
|
} |
|
|
|
var compareLayer = svg.append("g") |
|
.attr("class", "compare-layer"); |
|
|
|
if (config.compareArray) { |
|
|
|
// If a compare accessor exists, use it - otherwise use [x, y] instead |
|
config.compareArray.forEach(c => { |
|
if (config.compareAccessor) { |
|
c.point = config.compareAccessor(c); |
|
} else { |
|
c.point = [c.x, c.y]; |
|
} |
|
}); |
|
|
|
var compareGroups = compareLayer.selectAll("g") |
|
.data(config.compareArray) |
|
.enter().append("g") |
|
.attr("class", "compare-group") |
|
.attr("transform", d => "translate(" + d.point + ")") |
|
.style("opacity", 0); |
|
|
|
compareGroups.append("circle") |
|
.attr("class", "compare-circle") |
|
.attr("r", config.compareRadius) |
|
.on("click", function(d) { |
|
d3.select(this) |
|
.style("stroke", "red") |
|
.style("fill", "red") |
|
.style("fill-opacity", 0.2); |
|
stopTimerWithSelection(d.id); |
|
}) |
|
|
|
compareGroups.append("text") |
|
.attr("class", "compare-text") |
|
.attr("text-anchor", "middle") |
|
.attr("x", -config.compareRadius) |
|
.attr("y", -config.compareRadius) |
|
.text(d => d.label); |
|
} |
|
|
|
|
|
var t; |
|
var elapsedTime; |
|
var savedCoordinates; |
|
var savedId; |
|
var flashTimer; |
|
|
|
compareLayer.lower(); |
|
|
|
perception.on("click", () => { |
|
outlineRect.transition().style("fill-opacity", 0); |
|
|
|
info.select(".click-text").transition().attr("opacity", 0); |
|
|
|
info.attr("transform", "translate(" + [0, 0] + ")"); |
|
info.select(".task-text") |
|
.attr("text-anchor", "start") |
|
.attr("x", 10) |
|
.attr("y", 25) |
|
.style("font-size", 18); |
|
|
|
flashCircle(0); |
|
perception.lower(); |
|
compareLayer.raise(); |
|
|
|
svg.selectAll(".compare-group") |
|
.style("opacity", 1); |
|
|
|
t = d3.interval(function(elapsed) { |
|
timer.text((elapsed / 1000).toFixed(1) + "s"); |
|
elapsedTime = elapsed; |
|
|
|
perception.on("click", function() { |
|
stopTimer(this); |
|
}); |
|
}); |
|
}); |
|
|
|
function stopTimerWithSelection(id) { |
|
t.stop(); |
|
flashTimer.stop(); |
|
|
|
savedId = id; |
|
coordinates.text("Selected: " + id); |
|
|
|
perception.raise(); |
|
outlineRect.transition().style("fill-opacity", 0.5); |
|
} |
|
|
|
function stopTimer(clickArea) { |
|
t.stop(); |
|
flashTimer.stop(); |
|
|
|
savedCoordinates = d3.mouse(clickArea); |
|
coordinates.text("(" + Math.round(savedCoordinates[0]) + ", " + Math.round(savedCoordinates[1]) + ")"); |
|
|
|
perception.raise(); |
|
outlineRect.transition().style("fill-opacity", 0.5); |
|
|
|
marker.attr("transform", "translate(" + savedCoordinates + ")") |
|
.attr("opacity", 1); |
|
} |
|
|
|
function flashCircle() { |
|
var opacity = 0; |
|
flashTimer = d3.interval(function() { |
|
opacity = 1 - opacity; |
|
recordGroup.select("circle") |
|
.transition() |
|
.attr("opacity", opacity); |
|
}, 600); |
|
} |
|
} |
|
|
|
</script> |
|
</body> |