Turn a mercator projection into a dotted map.
Last active
December 18, 2015 17:39
-
-
Save jbalogh/5820481 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
circle { | |
fill: steelblue; | |
} | |
input { | |
vertical-align: middle; | |
} | |
#toggles { | |
display: none; | |
} | |
</style> | |
<body> | |
<div id="toggles"> | |
<form oninput="oa.value=width.valueAsNumber; ob.value=tolerance.valueAsNumber"> | |
<label>Box width <input type="range" name="width" min="2" max="30" value="5"></label> | |
<output name="oa" for="width"></output> | |
<label>Hit tolerance <input type="range" name="tolerance" min="1" value="16"></label> | |
<output name="ob" for="tolerance"></output> | |
<select name="projection"></select> | |
</form> | |
</div> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://d3js.org/topojson.v1.min.js"></script> | |
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script> | |
<script> | |
"use strict"; | |
var width = 800, | |
height = 600; | |
/* | |
var visibleCanvas = d3.select('body').append('canvas') | |
.attr('width', width) | |
.attr('height', height) | |
.node().getContext('2d'); | |
*/ | |
var ghostCanvas = d3.select('body').append('canvas') | |
.attr('width', width) | |
.attr('height', height) | |
.style('display', 'none') | |
.node().getContext('2d'); | |
var projection = d3.geo.equirectangular() | |
.scale((width + 1) / 2 / Math.PI) | |
.translate([width / 2, height / 2]) | |
.precision(.1); | |
var path = d3.geo.path() | |
.projection(projection) | |
.context(ghostCanvas); | |
var world; | |
var mapPixels; | |
var hitWidth = 3; | |
var hitTolerance = 8; | |
var projection = 'kavrayskiy7'; | |
var widthInput = document.querySelector('[name=width]'); | |
var toleranceInput = document.querySelector('[name=tolerance]'); | |
var projectionInput = document.querySelector('[name=projection]'); | |
widthInput.addEventListener('input', render); | |
toleranceInput.addEventListener('input', render); | |
projectionInput.addEventListener('change', project); | |
d3.json("world-50m.json", function(error, json) { | |
d3.select('#toggles').style('display', 'block').select('form').node().oninput(); | |
world = json; | |
project(); | |
}); | |
function project() { | |
projection = projectionInput.value; | |
console.log('projecting', projection); | |
console.time('projection'); | |
var projection = d3.geo[projection]() | |
.scale((width + 1) / 2 / Math.PI) | |
.translate([width / 2, height / 2]) | |
.precision(.1); | |
var path = d3.geo.path() | |
.projection(projection) | |
.context(ghostCanvas); | |
console.timeEnd('projection'); | |
console.time('ghost canvas'); | |
var c = ghostCanvas; | |
c.clearRect(0, 0, width, height); | |
c.fillStyle = 'rgb(100, 100, 100)', c.beginPath(), path(topojson.feature(world, world.objects.land)), c.fill(); | |
mapPixels = c.getImageData(0, 0, width, height).data; | |
console.timeEnd('ghost canvas'); | |
render(); | |
} | |
var skip = 'area bounds centroid circle distance greatArc interrupt interpolate length path projection rotation stream'.split(' '); | |
var options = Object.keys(d3.geo).filter(function(d){ return skip.indexOf(d) == -1; }); | |
options.sort(); | |
d3.select('[name=projection]').selectAll('option') | |
.data(options).enter() | |
.append('option') | |
.attr('value', function(d){ return d; }) | |
.text(function(d){ return d; }); | |
d3.select('option[value=kavrayskiy7]').attr('selected', 'selected'); | |
function render() { | |
console.time('render'); | |
hitWidth = widthInput.valueAsNumber; | |
hitTolerance = toleranceInput.valueAsNumber; | |
toleranceInput.max = hitWidth * hitWidth; | |
//drawCanvas(hitTest()); | |
drawSVG(hitTest()); | |
console.timeEnd('render'); | |
} | |
function hitTest() { | |
// Use hit-testing on the hidden canvas. | |
console.time('hit test'); | |
var half = Math.floor(hitWidth / 2); | |
var hits = []; | |
for (var y = 0; y < height; y += hitWidth) { | |
for (var x = 0; x < width; x += hitWidth) { | |
if (numHits(rgbsquare(mapPixels, x, y, hitWidth)) > hitTolerance) { | |
hits.push(x + half); | |
hits.push(y + half); | |
} | |
} | |
} | |
console.timeEnd('hit test'); | |
return hits; | |
} | |
function drawCanvas(hits) { | |
console.time('fill canvas'); | |
// Draw the pixelated map. | |
var c = visibleCanvas; | |
c.clearRect(0, 0, width, height); | |
c.fillStyle = 'steelblue'; | |
for (var i = 0, n = hits.length; i < n; i += 2) { | |
c.beginPath() | |
c.arc(hits[i], hits[i+1], hitWidth / 2, 0, 2 * Math.PI); | |
c.fill(); | |
} | |
console.timeEnd('fill canvas'); | |
} | |
var SCALE = 1.25; | |
function drawSVG(hits) { | |
console.time('fill svg') | |
var pairs = []; | |
for (var i = 0; i < hits.length; i+= 2) pairs.push([hits[i], hits[i+1]]) | |
d3.select('svg').remove(); | |
var svg = d3.select('body').append('svg') | |
.attr('width', width * SCALE) | |
.attr('height', height * SCALE); | |
svg.selectAll('circle') | |
.data(pairs).enter() | |
.append('circle') | |
.attr('cx', function(d){ return d[0] * SCALE; }) | |
.attr('cy', function(d){ return d[1] * SCALE; }) | |
.attr('r', hitWidth / 2); | |
console.timeEnd('fill svg') | |
} | |
function numHits(square) { | |
return sum(square) / 300; | |
} | |
function rgbsquare(arr, startX, startY, length) { | |
var rv = []; | |
for (var y = startY; y < startY + length; y++) { | |
for (var x = startX; x < startX + length; x++) { | |
var i = 4 * (y * width + x); | |
for (var z = i + 3; i < z; i++) rv.push(arr[i]) | |
} | |
} | |
return rv; | |
} | |
function sum(arr) { | |
for (var i = 0, sum = 0, n = arr.length; i < n; sum += arr[i++]); | |
return sum; | |
} | |
function avg(arr) { | |
return sum(arr) / arr.length | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment