|  | <!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> |