|
(function(){ |
|
var width = 960, |
|
height = 1160, |
|
padding = 6, // separation between nodes |
|
maxRadius = 20; |
|
|
|
var svg = d3.select("#mapContainer").append("svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
//*****hexagon grid stuff*************************// |
|
|
|
var hexRadius = Math.sqrt(3) * maxRadius, |
|
hexWidth = Math.sqrt(3) * hexRadius, |
|
hexHeight = 2 * hexRadius; |
|
//no of rows and cols in the hex grid |
|
var nrows = Math.ceil((height - 0.5 * hexRadius)/ (1.5 * hexRadius)), |
|
ncols = Math.ceil((width - 0.5 * Math.sqrt(3) * hexRadius)/ (Math.sqrt(3) * hexRadius)); |
|
|
|
drawHexGrid(nrows, ncols, hexRadius, svg); |
|
|
|
//finding the centroids of the hexcells |
|
function hexCentroids(nrows, ncols, hexRadius) { |
|
var centroidMatrix = []; //array of arrays |
|
for (var i = 0; i< nrows; i++){ |
|
var rowArr = []; |
|
var yCoord = hexRadius + 1.5 * hexRadius * i; |
|
var initX; |
|
if(i % 2 === 0) { //odd rows |
|
initX = 0.5 * Math.sqrt(3) * hexRadius; |
|
}else { //even rows |
|
initX = Math.sqrt(3) * hexRadius; |
|
} |
|
for (var j = 0; j < ncols; j++) { |
|
rowArr.push([(initX + j * Math.sqrt(3) * hexRadius), yCoord]) |
|
} |
|
|
|
centroidMatrix.push(rowArr) |
|
} |
|
|
|
return centroidMatrix; |
|
|
|
} |
|
|
|
/*** |
|
* find the closest hexcell centroid to a point with coordinates of xcoord and ycoord |
|
* @param xcoord |
|
* @param ycoord |
|
* @returns {*[]} array of size 2, with arr[0] corresponding to the colnumber of the hexgrid |
|
* and arr[1] the rownumber of the hexgrid |
|
*/ |
|
function getClosestCentroidIndex(xcoord, ycoord){ |
|
var yIndex = Math.round((ycoord - hexRadius) /(1.5 * hexRadius)); |
|
var initX; |
|
if(yIndex % 2 === 0) { //odd rows |
|
initX = 0.5 * Math.sqrt(3) * hexRadius; |
|
}else { //even rows |
|
initX = Math.sqrt(3) * hexRadius; |
|
} |
|
var xIndex = Math.round((xcoord - initX)/(Math.sqrt(3) * hexRadius)); |
|
|
|
return [xIndex, yIndex] |
|
} |
|
|
|
function hexagon(radius) { |
|
var x0 = 0, y0 = 0; |
|
var d3_hexbinAngles = d3.range(0, 2 * Math.PI, Math.PI / 3); |
|
return d3_hexbinAngles.map(function(angle) { |
|
var x1 = Math.sin(angle) * radius, |
|
y1 = -Math.cos(angle) * radius, |
|
dx = x1 - x0, |
|
dy = y1 - y0; |
|
x0 = x1; |
|
y0 = y1; |
|
return [dx, dy]; |
|
}); |
|
} |
|
|
|
function hexagonPath(radius) { |
|
return "m" + hexagon(radius).join("l") + "z"; |
|
} |
|
|
|
function drawHexGrid(nrows, ncols, hexRadius, svg) { |
|
var centroid = hexCentroids(nrows, ncols, hexRadius); |
|
var hexrows = svg.selectAll('g.hexrow').data(centroid) |
|
.enter().append('g').attr('class', 'hexrow'); |
|
|
|
hexrows.selectAll('path.hexagon').data(function(d){return d}) |
|
.enter().append('path') |
|
.attr('class', 'hexagon') |
|
.attr("d", function (d) { return "M" + d[0]+ "," + d[1] + hexagonPath(hexRadius); }) |
|
.attr('fill', 'rgba(255,255,255,0)').attr('stroke', 'black') |
|
|
|
|
|
|
|
} |
|
|
|
//***************************************************************// |
|
/** |
|
* rest the projection params so the map fills as much of the svg as possible |
|
* @param json geojson |
|
* @returns {{projection: *, path: *}} |
|
*/ |
|
function resetProjection(json){ |
|
//find the geo center of the current geojson data |
|
var center = d3.geo.centroid(json); |
|
//arbitrary scale, to be tweaked |
|
var scale = 150; |
|
//move to center for now |
|
var offset = [width/2, height/2]; |
|
// set the projection |
|
var projection = d3.geo.mercator().scale(scale).center(center) |
|
.translate(offset); |
|
|
|
// create the path |
|
var path = d3.geo.path().projection(projection); |
|
|
|
// using the path determine the bounds of the current map and use |
|
// these to determine better values for the scale and translation |
|
//bounds = [[left, top], [right, bottom]] |
|
|
|
var bounds = path.bounds(json); |
|
//how many times the width of the current path in pixels fit |
|
//within the scaling *B height |
|
var hscale = scale * width / (bounds[1][0] - bounds[0][0]); |
|
//similar for vertical |
|
var vscale = scale * height / (bounds[1][1] - bounds[0][1]); |
|
if(hscale < vscale) { |
|
scale = hscale; |
|
} else { |
|
scale = vscale; |
|
} |
|
//shift to new center |
|
offset = [width - (bounds[0][0] + bounds[1][0])/2, |
|
height - (bounds[0][1] + bounds[1][1])/2]; |
|
|
|
// new projection |
|
projection = d3.geo.mercator().center(center) |
|
.scale(scale).translate(offset); |
|
path = path.projection(projection); |
|
|
|
return { |
|
projection: projection, |
|
path: path |
|
} |
|
} |
|
var geodata; |
|
var projection = d3.geo.mercator().scale(1000); |
|
d3.json("uk_counties.topo.json", function(err, data){ |
|
geodata = topojson.feature(data, data.objects.uk_counties) |
|
|
|
var center = d3.geo.centroid(geodata); |
|
projection.center(center); |
|
|
|
projection = resetProjection(geodata).projection; |
|
var path = d3.geo.path().projection(projection); |
|
|
|
var mapG = svg.append('g').attr('class', 'mapG'); |
|
var nodeG = svg.append('g').attr('class', 'nodeG'); |
|
|
|
mapG.selectAll("path") |
|
.data(geodata.features) |
|
.enter().append("path") |
|
.attr("d", d3.geo.path().projection(projection)) |
|
.attr("stroke", "black").style("fill", "none"); |
|
|
|
var features = geodata.features; |
|
|
|
var hexblocks = features.map(function(d){ |
|
var centroid = path.centroid(d); |
|
return { |
|
name: d.properties.NAME, |
|
cx: centroid[0], |
|
cy: centroid[1], |
|
x: centroid[0], |
|
y: centroid[1], |
|
radius: maxRadius, |
|
} |
|
}); |
|
|
|
//set to true when curating the hexagon layouts |
|
var isHexBlockmoving = false; |
|
var hexBlockMovingID = null; |
|
//force layout with the centroids as the nodes, |
|
//the size and width of the svg |
|
var force = d3.layout.force() |
|
.nodes(hexblocks) |
|
.size([width, height]) |
|
.gravity(0) |
|
.charge(0) //instead of the default 30 |
|
.on('tick', tick) |
|
.start(); |
|
|
|
force.on('end', function() { |
|
var hexG = d3.selectAll('g.hexrow'); |
|
hexblocks.forEach(function(hex) { |
|
var closest = getClosestCentroidIndex(hex.x, hex.y); |
|
hex.newX = closest[0]; |
|
hex.newY = closest[1]; |
|
hexG.filter(function(d, i){ |
|
return i === closest[1] |
|
}).selectAll('path').filter(function(d, i){ |
|
return i === closest[0] |
|
}).attr('fill', 'blue').attr('id', hex.name) |
|
// .on('mouseover', function(d, i){ |
|
// console.log(d3.select(this).attr('id')) |
|
// }) |
|
|
|
}); |
|
d3.selectAll('.hexagon').on('click', moveHexBlock); |
|
//take out the circles so they don't affect the hexagon on clikc |
|
d3.selectAll('.nodeG').remove(); |
|
}); |
|
|
|
// Move nodes toward cluster focus. |
|
//basically the force layout changes cy as it tries to move the |
|
//circles apart, but you are pulling them back towards their centroids with th |
|
//+= (d.clusterY - d.cy) * alpha |
|
//the higher the alpah, the more strongly pulled it is |
|
// Move nodes toward cluster focus. |
|
function gravity(alpha) { |
|
return function(d) { |
|
d.y += (d.cy - d.y) * alpha; |
|
d.x += (d.cx - d.x) * alpha; |
|
}; |
|
} |
|
|
|
function tick(e) { |
|
|
|
node.each(gravity(.2 * e.alpha)) |
|
.each(collide(e.alpha)) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }); |
|
} |
|
|
|
// Resolve collisions between nodes. |
|
function collide(alpha) { |
|
var quadtree = d3.geom.quadtree(hexblocks); |
|
return function(d) { |
|
var r = 2 * maxRadius + padding, |
|
nx1 = d.x - r, |
|
nx2 = d.x + r, |
|
ny1 = d.y - r, |
|
ny2 = d.y + r; |
|
quadtree.visit(function(quad, x1, y1, x2, y2) { |
|
if (quad.point && (quad.point !== d)) { |
|
var x = d.x - quad.point.x, |
|
y = d.y - quad.point.y, |
|
l = Math.sqrt(x * x + y * y), |
|
r = maxRadius + quad.point.radius + padding; |
|
if (l < r) { |
|
l = (l - r) / l * alpha; |
|
d.x -= x *= l; |
|
d.y -= y *= l; |
|
quad.point.x += x; |
|
quad.point.y += y; |
|
} |
|
} |
|
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1; |
|
}); |
|
}; |
|
} |
|
|
|
function moveHexBlock(d, i) { |
|
console.log('move this block!', d3.select(this).attr('id')) |
|
var hexID = d3.select(this).attr('id'); |
|
if(hexID && hexID != null && isHexBlockmoving == false) { |
|
//so step 1 |
|
isHexBlockmoving = true; |
|
hexBlockMovingID = hexID; |
|
|
|
d3.select(this).attr('id', 'moving') //set the current element id to be 'moving' |
|
.attr('fill', 'rgba(0,0,255,0.5)'); // and set the moving fill to be |
|
} |
|
|
|
if((hexID == null ||!hexID) && isHexBlockmoving === true) { |
|
d3.select(this).attr('id', hexBlockMovingID) |
|
.attr('fill', 'blue') |
|
|
|
isHexBlockmoving = false; |
|
d3.select('#moving').attr('id', null).attr('fill', 'rgba(255,255,255,0)'); |
|
var hexBlockEl = hexblocks.filter(function(item) { |
|
return item.name === hexBlockMovingID; |
|
}); |
|
|
|
hexBlockEl.newX = d[0]; |
|
hexBlockEl.newY = d[1]; |
|
} |
|
} |
|
|
|
var node = nodeG.selectAll('.node') |
|
.data(hexblocks) |
|
.enter().append('circle') |
|
.attr('class', 'node'); |
|
node.attr('r', maxRadius).attr('fill', 'grey') |
|
|
|
d3.select('#exportDataBtn').on('click', function() { |
|
var data = JSON.stringify(hexblocks); |
|
console.log(data) |
|
}); |
|
|
|
}); |
|
})(); |