Based on this Martín González's block, shows how to properly select the colours taking the neighbours in account as in this other block, which is based on this other one by Jason Davies!
Last active
May 26, 2016 12:24
-
-
Save rveciana/47d7ad338b97162ef9ab7d0bf00fe824 to your computer and use it in GitHub Desktop.
spam.js with authomatic color selection
This file contains 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"> | |
<body> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script> | |
<script src="http://d3js.org/topojson.v1.min.js"></script> | |
<script src="spam.js"></script> | |
<script src="rbush.min.js"></script> | |
<script> | |
var width = 960, | |
height = 600 | |
var i = 0 | |
var color = d3.scale.category20() | |
var graticule = d3.geo.graticule() | |
// Canvas radial gradient | |
var x = width / 2, | |
y = height / 2, | |
// Radii of the start glow. | |
innerRadius = 220, | |
// Radii of the end glow. | |
outerRadius = 300, | |
// Radius of the entire circle. | |
radius = 200 | |
d3.json("world-50m.json", function(error, d) { | |
topojson.presimplify(d) | |
var neighbors = topojson.neighbors(d.objects.countries.geometries) | |
var countries = topojson.feature(d, d.objects.countries) | |
countries.features.forEach(function(d, i) { | |
d.properties.color = d3.max(neighbors[i], function(n) { return countries.features[n].properties.color; }) + 1|0 | |
}); | |
var map = new StaticCanvasMap({ | |
element: "body", | |
width: width, | |
height: height, | |
projection: d3.geo.orthographic() | |
.clipAngle(90) | |
.precision(0.1) | |
.scale(250), | |
data: [ | |
{ | |
features: countries, | |
static: { | |
prepaint: function(parameters) { | |
parameters.context.beginPath() | |
parameters.path({type: "Sphere"}) | |
parameters.context.lineWidth = 2 | |
parameters.context.strokeStyle = "rgb(198, 197, 197)" | |
parameters.context.stroke() | |
var gradient = parameters.context.createRadialGradient(x, y, innerRadius, x, y, outerRadius) | |
gradient.addColorStop(0, 'rgb(248, 248, 248)') | |
gradient.addColorStop(1, 'rgb(210, 210, 210)') | |
parameters.context.fillStyle = gradient | |
parameters.context.fill() | |
parameters.context.beginPath() | |
parameters.path(graticule()) | |
parameters.context.lineWidth = 0.2 | |
parameters.context.strokeStyle = 'rgba(30,30,30, 0.5)' | |
parameters.context.stroke() | |
}, | |
paintfeature: function(parameters, d) { | |
parameters.context.lineWidth = 0.8 / parameters.scale | |
parameters.context.strokeStyle = "rgba(0,0,0, 0.6)" | |
parameters.context.stroke() | |
parameters.context.fillStyle = color(d.properties.color) | |
parameters.context.fill() | |
} | |
} | |
} | |
] | |
}) | |
map.init() | |
}) | |
</script> |
This file contains 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
(function(){"use strict";function rbush(maxEntries,format){if(!(this instanceof rbush))return new rbush(maxEntries,format);this._maxEntries=Math.max(4,maxEntries||9);this._minEntries=Math.max(2,Math.ceil(this._maxEntries*.4));if(format){this._initFormat(format)}this.clear()}rbush.prototype={all:function(){return this._all(this.data,[])},search:function(bbox){var node=this.data,result=[],toBBox=this.toBBox;if(!intersects(bbox,node.bbox))return result;var nodesToSearch=[],i,len,child,childBBox;while(node){for(i=0,len=node.children.length;i<len;i++){child=node.children[i];childBBox=node.leaf?toBBox(child):child.bbox;if(intersects(bbox,childBBox)){if(node.leaf)result.push(child);else if(contains(bbox,childBBox))this._all(child,result);else nodesToSearch.push(child)}}node=nodesToSearch.pop()}return result},collides:function(bbox){var node=this.data,toBBox=this.toBBox;if(!intersects(bbox,node.bbox))return false;var nodesToSearch=[],i,len,child,childBBox;while(node){for(i=0,len=node.children.length;i<len;i++){child=node.children[i];childBBox=node.leaf?toBBox(child):child.bbox;if(intersects(bbox,childBBox)){if(node.leaf||contains(bbox,childBBox))return true;nodesToSearch.push(child)}}node=nodesToSearch.pop()}return false},load:function(data){if(!(data&&data.length))return this;if(data.length<this._minEntries){for(var i=0,len=data.length;i<len;i++){this.insert(data[i])}return this}var node=this._build(data.slice(),0,data.length-1,0);if(!this.data.children.length){this.data=node}else if(this.data.height===node.height){this._splitRoot(this.data,node)}else{if(this.data.height<node.height){var tmpNode=this.data;this.data=node;node=tmpNode}this._insert(node,this.data.height-node.height-1,true)}return this},insert:function(item){if(item)this._insert(item,this.data.height-1);return this},clear:function(){this.data={children:[],height:1,bbox:empty(),leaf:true};return this},remove:function(item){if(!item)return this;var node=this.data,bbox=this.toBBox(item),path=[],indexes=[],i,parent,index,goingUp;while(node||path.length){if(!node){node=path.pop();parent=path[path.length-1];i=indexes.pop();goingUp=true}if(node.leaf){index=node.children.indexOf(item);if(index!==-1){node.children.splice(index,1);path.push(node);this._condense(path);return this}}if(!goingUp&&!node.leaf&&contains(node.bbox,bbox)){path.push(node);indexes.push(i);i=0;parent=node;node=node.children[0]}else if(parent){i++;node=parent.children[i];goingUp=false}else node=null}return this},toBBox:function(item){return item},compareMinX:function(a,b){return a[0]-b[0]},compareMinY:function(a,b){return a[1]-b[1]},toJSON:function(){return this.data},fromJSON:function(data){this.data=data;return this},_all:function(node,result){var nodesToSearch=[];while(node){if(node.leaf)result.push.apply(result,node.children);else nodesToSearch.push.apply(nodesToSearch,node.children);node=nodesToSearch.pop()}return result},_build:function(items,left,right,height){var N=right-left+1,M=this._maxEntries,node;if(N<=M){node={children:items.slice(left,right+1),height:1,bbox:null,leaf:true};calcBBox(node,this.toBBox);return node}if(!height){height=Math.ceil(Math.log(N)/Math.log(M));M=Math.ceil(N/Math.pow(M,height-1))}node={children:[],height:height,bbox:null,leaf:false};var N2=Math.ceil(N/M),N1=N2*Math.ceil(Math.sqrt(M)),i,j,right2,right3;multiSelect(items,left,right,N1,this.compareMinX);for(i=left;i<=right;i+=N1){right2=Math.min(i+N1-1,right);multiSelect(items,i,right2,N2,this.compareMinY);for(j=i;j<=right2;j+=N2){right3=Math.min(j+N2-1,right2);node.children.push(this._build(items,j,right3,height-1))}}calcBBox(node,this.toBBox);return node},_chooseSubtree:function(bbox,node,level,path){var i,len,child,targetNode,area,enlargement,minArea,minEnlargement;while(true){path.push(node);if(node.leaf||path.length-1===level)break;minArea=minEnlargement=Infinity;for(i=0,len=node.children.length;i<len;i++){child=node.children[i];area=bboxArea(child.bbox);enlargement=enlargedArea(bbox,child.bbox)-area;if(enlargement<minEnlargement){minEnlargement=enlargement;minArea=area<minArea?area:minArea;targetNode=child}else if(enlargement===minEnlargement){if(area<minArea){minArea=area;targetNode=child}}}node=targetNode}return node},_insert:function(item,level,isNode){var toBBox=this.toBBox,bbox=isNode?item.bbox:toBBox(item),insertPath=[];var node=this._chooseSubtree(bbox,this.data,level,insertPath);node.children.push(item);extend(node.bbox,bbox);while(level>=0){if(insertPath[level].children.length>this._maxEntries){this._split(insertPath,level);level--}else break}this._adjustParentBBoxes(bbox,insertPath,level)},_split:function(insertPath,level){var node=insertPath[level],M=node.children.length,m=this._minEntries;this._chooseSplitAxis(node,m,M);var splitIndex=this._chooseSplitIndex(node,m,M);var newNode={children:node.children.splice(splitIndex,node.children.length-splitIndex),height:node.height,bbox:null,leaf:false};if(node.leaf)newNode.leaf=true;calcBBox(node,this.toBBox);calcBBox(newNode,this.toBBox);if(level)insertPath[level-1].children.push(newNode);else this._splitRoot(node,newNode)},_splitRoot:function(node,newNode){this.data={children:[node,newNode],height:node.height+1,bbox:null,leaf:false};calcBBox(this.data,this.toBBox)},_chooseSplitIndex:function(node,m,M){var i,bbox1,bbox2,overlap,area,minOverlap,minArea,index;minOverlap=minArea=Infinity;for(i=m;i<=M-m;i++){bbox1=distBBox(node,0,i,this.toBBox);bbox2=distBBox(node,i,M,this.toBBox);overlap=intersectionArea(bbox1,bbox2);area=bboxArea(bbox1)+bboxArea(bbox2);if(overlap<minOverlap){minOverlap=overlap;index=i;minArea=area<minArea?area:minArea}else if(overlap===minOverlap){if(area<minArea){minArea=area;index=i}}}return index},_chooseSplitAxis:function(node,m,M){var compareMinX=node.leaf?this.compareMinX:compareNodeMinX,compareMinY=node.leaf?this.compareMinY:compareNodeMinY,xMargin=this._allDistMargin(node,m,M,compareMinX),yMargin=this._allDistMargin(node,m,M,compareMinY);if(xMargin<yMargin)node.children.sort(compareMinX)},_allDistMargin:function(node,m,M,compare){node.children.sort(compare);var toBBox=this.toBBox,leftBBox=distBBox(node,0,m,toBBox),rightBBox=distBBox(node,M-m,M,toBBox),margin=bboxMargin(leftBBox)+bboxMargin(rightBBox),i,child;for(i=m;i<M-m;i++){child=node.children[i];extend(leftBBox,node.leaf?toBBox(child):child.bbox);margin+=bboxMargin(leftBBox)}for(i=M-m-1;i>=m;i--){child=node.children[i];extend(rightBBox,node.leaf?toBBox(child):child.bbox);margin+=bboxMargin(rightBBox)}return margin},_adjustParentBBoxes:function(bbox,path,level){for(var i=level;i>=0;i--){extend(path[i].bbox,bbox)}},_condense:function(path){for(var i=path.length-1,siblings;i>=0;i--){if(path[i].children.length===0){if(i>0){siblings=path[i-1].children;siblings.splice(siblings.indexOf(path[i]),1)}else this.clear()}else calcBBox(path[i],this.toBBox)}},_initFormat:function(format){var compareArr=["return a"," - b",";"];this.compareMinX=new Function("a","b",compareArr.join(format[0]));this.compareMinY=new Function("a","b",compareArr.join(format[1]));this.toBBox=new Function("a","return [a"+format.join(", a")+"];")}};function calcBBox(node,toBBox){node.bbox=distBBox(node,0,node.children.length,toBBox)}function distBBox(node,k,p,toBBox){var bbox=empty();for(var i=k,child;i<p;i++){child=node.children[i];extend(bbox,node.leaf?toBBox(child):child.bbox)}return bbox}function empty(){return[Infinity,Infinity,-Infinity,-Infinity]}function extend(a,b){a[0]=Math.min(a[0],b[0]);a[1]=Math.min(a[1],b[1]);a[2]=Math.max(a[2],b[2]);a[3]=Math.max(a[3],b[3]);return a}function compareNodeMinX(a,b){return a.bbox[0]-b.bbox[0]}function compareNodeMinY(a,b){return a.bbox[1]-b.bbox[1]}function bboxArea(a){return(a[2]-a[0])*(a[3]-a[1])}function bboxMargin(a){return a[2]-a[0]+(a[3]-a[1])}function enlargedArea(a,b){return(Math.max(b[2],a[2])-Math.min(b[0],a[0]))*(Math.max(b[3],a[3])-Math.min(b[1],a[1]))}function intersectionArea(a,b){var minX=Math.max(a[0],b[0]),minY=Math.max(a[1],b[1]),maxX=Math.min(a[2],b[2]),maxY=Math.min(a[3],b[3]);return Math.max(0,maxX-minX)*Math.max(0,maxY-minY)}function contains(a,b){return a[0]<=b[0]&&a[1]<=b[1]&&b[2]<=a[2]&&b[3]<=a[3]}function intersects(a,b){return b[0]<=a[2]&&b[1]<=a[3]&&b[2]>=a[0]&&b[3]>=a[1]}function multiSelect(arr,left,right,n,compare){var stack=[left,right],mid;while(stack.length){right=stack.pop();left=stack.pop();if(right-left<=n)continue;mid=left+Math.ceil((right-left)/n/2)*n;select(arr,left,right,mid,compare);stack.push(left,mid,mid,right)}}function select(arr,left,right,k,compare){var n,i,z,s,sd,newLeft,newRight,t,j;while(right>left){if(right-left>600){n=right-left+1;i=k-left+1;z=Math.log(n);s=.5*Math.exp(2*z/3);sd=.5*Math.sqrt(z*s*(n-s)/n)*(i-n/2<0?-1:1);newLeft=Math.max(left,Math.floor(k-i*s/n+sd));newRight=Math.min(right,Math.floor(k+(n-i)*s/n+sd));select(arr,newLeft,newRight,k,compare)}t=arr[k];i=left;j=right;swap(arr,left,k);if(compare(arr[right],t)>0)swap(arr,left,right);while(i<j){swap(arr,i,j);i++;j--;while(compare(arr[i],t)<0)i++;while(compare(arr[j],t)>0)j--}if(compare(arr[left],t)===0)swap(arr,left,j);else{j++;swap(arr,j,right)}if(j<=k)left=j+1;if(k<=j)right=j-1}}function swap(arr,i,j){var tmp=arr[i];arr[i]=arr[j];arr[j]=tmp}if(typeof define==="function"&&define.amd)define("rbush",function(){return rbush});else if(typeof module!=="undefined")module.exports=rbush;else if(typeof self!=="undefined")self.rbush=rbush;else window.rbush=rbush})(); |
This file contains 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
var StaticCanvasMap; | |
var ZoomableCanvasMap; | |
! function() { | |
"use strict"; | |
// TODO use turf inside as a dependency? | |
// Copied from turf.inside | |
function inside(pt, polygon) { | |
var polys = polygon.geometry.coordinates | |
// normalize to multipolygon | |
if (polygon.geometry.type === 'Polygon') | |
polys = [polys] | |
var insidePoly = false | |
var i = 0 | |
while (i < polys.length && !insidePoly) { | |
// check if it is in the outer ring first | |
if (inRing(pt, polys[i][0])) { | |
var inHole = false | |
var k = 1 | |
// check for the point in any of the holes | |
while (k < polys[i].length && !inHole) { | |
if (inRing(pt, polys[i][k])) { | |
inHole = true | |
} | |
k++ | |
} | |
if(!inHole) | |
insidePoly = true | |
} | |
i++ | |
} | |
return insidePoly | |
} | |
// pt is [x,y] and ring is [[x,y], [x,y],..] | |
function inRing (pt, ring) { | |
var isInside = false | |
for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) { | |
var xi = ring[i][0], yi = ring[i][1] | |
var xj = ring[j][0], yj = ring[j][1] | |
var intersect = ((yi > pt[1]) !== (yj > pt[1])) && | |
(pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi) | |
if (intersect) isInside = !isInside | |
} | |
return isInside | |
} | |
function maxBounds(one, two) { | |
var bounds = two | |
if (one[0][0] < two[0][0]) | |
bounds[0][0] = one[0][0] | |
if (one[0][1] < two[0][1]) | |
bounds[0][1] = one[0][1] | |
if (one[1][0] > two[1][0]) | |
bounds[1][0] = one[1][0] | |
if (one[1][1] > two[1][1]) | |
bounds[1][1] = one[1][1] | |
return bounds | |
} | |
function createRTree(element, dataPath) { | |
element.lookupTree = rbush(4) | |
var elements = [] | |
for (var j in element.features.features) { | |
var bounds = dataPath.bounds(element.features.features[j]) | |
elements.push([ | |
bounds[0][0].toFixed(0), | |
bounds[0][1].toFixed(0), | |
Math.ceil(bounds[1][0]), | |
Math.ceil(bounds[1][1]), | |
element.features.features[j] | |
]) | |
} | |
element.lookupTree.load(elements) | |
} | |
function paintFeature(element, feature, parameters) { | |
parameters.context.beginPath() | |
parameters.path(feature) | |
element.static.paintfeature(parameters, feature) | |
} | |
function paintBackgroundElement(element, parameters) { | |
if (!element.static) | |
return | |
if (element.static.prepaint) | |
element.static.prepaint(parameters) | |
if (element.static.paintfeature) { | |
var lookup = element.lookupTree.search([ | |
parameters.translate[0], | |
parameters.translate[1], | |
parameters.width / parameters.scale - parameters.translate[0], | |
parameters.height / parameters.scale - parameters.translate[1] | |
]) | |
for (var j in lookup) { | |
paintFeature(element, lookup[j][4], parameters) | |
} | |
} | |
if (element.static.postpaint) | |
element.static.postpaint(parameters) | |
} | |
function PartialPainter(data, parameters) { | |
var index = 0, | |
j = 0, | |
element = null, | |
currentLookup = [] | |
this.hasNext = function() { | |
return index <= data.length && j < currentLookup.length | |
} | |
this.renderNext = function() { | |
if (index >= data.length && j >= currentLookup.length) | |
return | |
var start = performance.now() | |
if (!element || j >= currentLookup.length) { | |
while (index < data.length && !data[index].static) { | |
index++ | |
} | |
if (index >= data.length) | |
return | |
element = data[index] | |
if (element.static.prepaint) | |
element.static.prepaint(parameters) | |
currentLookup = element.lookupTree.search([ | |
- parameters.translate[0], | |
- parameters.translate[1], | |
parameters.width / parameters.scale - parameters.translate[0], | |
parameters.height / parameters.scale - parameters.translate[1] | |
]) | |
j = 0 | |
++index | |
} | |
if (element.static.paintfeature) { | |
for (; j != currentLookup.length; ++j) { | |
var feature = currentLookup[j][4] | |
paintFeature(element, feature, parameters) | |
if ((performance.now() - start) > 10) | |
break | |
} | |
} else { | |
j = currentLookup.length | |
} | |
if (j == currentLookup.length && element.static.postpaint) { | |
element.static.postpaint(parameters) | |
} | |
} | |
this.finish = function() { | |
if (index >= data.length && j >= currentLookup.length) | |
return | |
if (j < currentLookup.length) | |
index-- | |
for (; index != data.length; ++index) { | |
if (j >= currentLookup.length) { | |
while (!data[index].static && index < data.length) { | |
index++ | |
} | |
if (index >= data.length) | |
return | |
element = data[index] | |
if (element.static.prepaint) | |
element.static.prepaint(parameters) | |
currentLookup = element.lookupTree.search([ | |
- parameters.translate[0], | |
- parameters.translate[1], | |
parameters.width / parameters.scale - parameters.translate[0], | |
parameters.height / parameters.scale - parameters.translate[1] | |
]) | |
j = 0 | |
} | |
if (element.static.paintfeature) { | |
for (; j != currentLookup.length; ++j) { | |
var feature = currentLookup[j][4] | |
paintFeature(element, feature, parameters) | |
} | |
} | |
if (element.static.postpaint) | |
element.static.postpaint(parameters) | |
} | |
} | |
} | |
function translatePoint(point, scale, translate) { | |
return [ | |
point[0] / scale - translate[0], | |
point[1] / scale - translate[1] | |
] | |
} | |
function extend(extension, obj) { | |
var newObj = {} | |
// FIXME this is a bit hacky? Can't we just mutate the original obj? (can't bc projection) | |
for (var elem in obj) { | |
newObj[elem] = obj[elem] | |
} | |
for (var elem in extension) { | |
if (!newObj.hasOwnProperty(elem)) | |
newObj[elem] = extension[elem] | |
} | |
return newObj | |
} | |
function CanvasMap(parameters) { | |
var settings = extend({ | |
width: d3.select(parameters.element).node().getBoundingClientRect().width, | |
ratio: 1, | |
area: 0, | |
scale: 1, | |
translate: [0, 0], | |
background: null, | |
backgroundScale: 1, | |
backgroundTranslate: [0, 0], | |
map: this | |
}, parameters), | |
simplify = d3.geo.transform({ | |
point: function(x, y, z) { | |
if (!z || z >= settings.area) { | |
this.stream.point(x, y) | |
} | |
} | |
}), | |
canvas = null, | |
context = null | |
if (!parameters.projection) { | |
var b = [[Infinity, Infinity], | |
[-Infinity, -Infinity]] | |
for (var i in settings.data) { | |
b = maxBounds(b, d3.geo.bounds(settings.data[i].features)) | |
} | |
settings.projection = d3.geo.mercator() | |
.scale(1) | |
.center([(b[1][0] + b[0][0]) / 2, (b[1][1] + b[0][1]) / 2]) | |
} | |
var dataPath = d3.geo.path().projection({ | |
stream: function(s) { | |
return simplify.stream(settings.projection.stream(s)) | |
} | |
}) | |
var b = [[Infinity, Infinity], | |
[-Infinity, -Infinity]] | |
for (var i in settings.data) { | |
b = maxBounds(b, dataPath.bounds(settings.data[i].features)) | |
} | |
var dx = b[1][0] - b[0][0], | |
dy = b[1][1] - b[0][1] | |
if (!parameters.projection) { | |
settings.height = settings.height || Math.ceil(dy * settings.width / dx) | |
settings.projection.scale(0.9 * (settings.width / dx)) | |
.translate([settings.width / 2, settings.height / 2]) | |
} else if (!settings.height) { | |
settings.height = Math.ceil(dy * 1 / 0.9) | |
} | |
d3.select(settings.parameters).attr("height", settings.height) | |
function init() { | |
canvas = d3.select(settings.element) | |
.append("canvas") | |
context = canvas.node().getContext("2d") | |
var devicePixelRatio = window.devicePixelRatio || 1, | |
backingStoreRatio = context.webkitBackingStorePixelRatio || | |
context.mozBackingStorePixelRatio || | |
context.msBackingStorePixelRatio || | |
context.oBackingStorePixelRatio || | |
context.backingStorePixelRatio || 1 | |
settings.ratio = devicePixelRatio / backingStoreRatio | |
settings.area = 1 / settings.projection.scale() / settings.ratio / 20 | |
canvas.attr("width", settings.width * settings.ratio) | |
canvas.attr("height", settings.height * settings.ratio) | |
canvas.style("width", settings.width + "px") | |
canvas.style("height", settings.height + "px") | |
context.lineJoin = "round" | |
context.lineCap = "round" | |
dataPath.context(context) | |
context.clearRect(0, 0, settings.width * settings.ratio, settings.height * settings.ratio) | |
context.save() | |
context.scale(settings.ratio, settings.ratio) | |
// TODO move rtree part out? | |
for (var i in settings.data) { | |
createRTree(settings.data[i], dataPath) | |
} | |
settings.background = new Image() | |
settings.backgroundScale = settings.scale | |
settings.backgroundTranslate = settings.translate | |
var parameters = { | |
path: dataPath, | |
context: context, | |
scale: settings.scale, | |
translate: settings.translate, | |
width: settings.width, | |
height: settings.height, | |
map: settings.map | |
} | |
var callback = function() { | |
var hasHover = false, | |
hasClick = false | |
for (var i in settings.data) { | |
var element = settings.data[i] | |
hasHover = hasHover || (element.events && element.events.hover) | |
hasClick = hasClick || (element.events && element.events.click) | |
if (element.dynamic && element.dynamic.postpaint) | |
element.dynamic.postpaint(parameters, null) | |
} | |
context.restore() | |
hasClick && canvas.on("click", click) | |
hasHover && canvas.on("mousemove", hover) | |
.on("mouseleave", hoverLeave) | |
} | |
for (var i in settings.data) { | |
var element = settings.data[i] | |
if (element.dynamic && element.dynamic.prepaint) | |
element.dynamic.prepaint(parameters, element.hoverElement) | |
} | |
for (var i in settings.data) { | |
var element = settings.data[i] | |
paintBackgroundElement(element, parameters) | |
} | |
settings.background.onload = callback | |
settings.background.src = canvas.node().toDataURL() | |
//Prevent another call to the init method | |
this.init = function() {} | |
} | |
// TODO probably try to use the same data path in the zoom class, but have a different area settable? | |
function paint() { | |
context.save() | |
context.scale(settings.scale * settings.ratio, settings.scale * settings.ratio) | |
context.translate(settings.translate[0], settings.translate[1]) | |
context.clearRect(- settings.translate[0], - settings.translate[1], settings.width * settings.ratio, settings.height * settings.ratio) | |
context.rect(- settings.translate[0], - settings.translate[1], | |
settings.width / settings.scale, | |
settings.height / settings.scale) | |
context.clip() | |
// FIXME this needs a way for the callback to use the lookupTree? | |
var parameters = { | |
path: dataPath, | |
context: dataPath.context(), | |
scale: settings.scale, | |
translate: settings.translate, | |
width: settings.width, | |
height: settings.height, | |
map: settings.map | |
} | |
settings.area = 1 / settings.projection.scale() / settings.scale / settings.ratio / 20 | |
for (var i in settings.data) { | |
var element = settings.data[i] | |
if (element.dynamic && element.dynamic.prepaint) | |
element.dynamic.prepaint(parameters, element.hoverElement) | |
} | |
context.drawImage(settings.background, 0, 0, | |
settings.width * settings.ratio, settings.height * settings.ratio, | |
- settings.backgroundTranslate[0], | |
- settings.backgroundTranslate[1], | |
settings.width / settings.backgroundScale, settings.height / settings.backgroundScale) | |
for (var i in settings.data) { | |
var element = settings.data[i] | |
if (element.dynamic && element.dynamic.postpaint) | |
element.dynamic.postpaint(parameters, element.hoverElement) | |
} | |
context.restore() | |
} | |
function click() { | |
var point = translatePoint(d3.mouse(this), settings.scale, settings.translate) | |
var parameters = { | |
scale: settings.scale, | |
translate: settings.translate, | |
width: settings.width, | |
height: settings.height, | |
map: settings.map | |
} | |
for (var i in settings.data) { | |
var element = settings.data[i] | |
if (!element.events || !element.events.click) | |
continue | |
var lookup = element.lookupTree.search([point[0], point[1], point[0], point[1]]) | |
var isInside = false | |
for (var j in lookup) { | |
var feature = lookup[j][4] | |
if (inside(settings.projection.invert(point), feature)) { | |
element.events.click(parameters, feature) | |
isInside = true | |
} | |
} | |
isInside || element.events.click(parameters, null) | |
} | |
} | |
function hoverLeave() { | |
var parameters = { | |
scale: settings.scale, | |
translate: settings.translate, | |
width: settings.width, | |
height: settings.height, | |
map: settings.map | |
} | |
for (var i in settings.data) { | |
var element = settings.data[i] | |
if (!element.events || !element.events.hover) | |
continue | |
element.hoverElement = false | |
element.events.hover(parameters, null) | |
} | |
} | |
function hover() { | |
var point = translatePoint(d3.mouse(this), settings.scale, settings.translate), | |
parameters = { | |
scale: settings.scale, | |
translate: settings.translate, | |
width: settings.width, | |
height: settings.height, | |
map: settings.map | |
} | |
for (var i in settings.data) { | |
var element = settings.data[i] | |
if (!element.events || !element.events.hover || | |
(element.hoverElement && inside(settings.projection.invert(point), element.hoverElement))) { | |
continue | |
} | |
element.hoverElement = false | |
var lookup = element.lookupTree.search([point[0], point[1], point[0], point[1]]) | |
for (var j in lookup) { | |
var feature = lookup[j][4] | |
if (inside(settings.projection.invert(point), feature)) { | |
element.hoverElement = feature | |
break | |
} | |
} | |
element.events.hover(parameters, element.hoverElement) | |
} | |
} | |
this.init = init | |
this.paint = paint | |
this.settings = function() { | |
return settings | |
} | |
} | |
StaticCanvasMap = function(parameters) { | |
var map = new CanvasMap(parameters) | |
this.init = function() { | |
map.init() | |
} | |
this.paint = function() { | |
map.paint() | |
} | |
} | |
var epsilon = 0.5 | |
function nearEqual(a, b) { | |
return Math.abs(a - b) < epsilon | |
} | |
function ImageCache(parameters) { | |
var cache = [], | |
settings = parameters | |
this.addImage = function(parameters) { | |
cache.push(parameters) | |
} | |
this.getImage = function(parameters) { | |
for (var i in cache) { | |
var element = cache[i] | |
if (nearEqual(element.scale, parameters.scale) && | |
nearEqual(element.translate[0], parameters.translate[0]) && | |
nearEqual(element.translate[1], parameters.translate[1])) | |
return element | |
} | |
return null | |
} | |
this.getFittingImage = function(bbox) { | |
// Auto set scale=1, translate[0, 0] image as default return | |
var currentImage = cache.length > 0 ? cache[0] : null | |
for (var i in cache) { | |
var image = cache[i] | |
var imageBB = [ | |
- image.translate[0], | |
- image.translate[1], | |
settings.width / image.scale - image.translate[0], | |
settings.height / image.scale - image.translate[1] | |
] | |
if (imageBB[0] <= bbox[0] && | |
imageBB[1] <= bbox[1] && | |
imageBB[2] >= bbox[2] && | |
imageBB[3] >= bbox[3] && | |
(!currentImage || currentImage.scale < image.scale)) { | |
currentImage = image | |
} | |
} | |
return currentImage | |
} | |
} | |
ZoomableCanvasMap = function(parameters) { | |
var map = new CanvasMap(parameters), | |
simplify = d3.geo.transform({ | |
point: function(x, y, z) { | |
if (z >= area) this.stream.point(x, y) | |
} | |
}), | |
area = 0, | |
canvas = null, | |
context = null, | |
settings = map.settings(), | |
dataPath = d3.geo.path().projection({ | |
stream: function(s) { | |
return simplify.stream(settings.projection.stream(s)) | |
} | |
}), | |
imageCache = new ImageCache({ | |
width: settings.width, | |
height: settings.height | |
}), | |
busy = false | |
settings.map = this | |
settings.zoomScale = settings.zoomScale || 0.5 | |
this.init = function() { | |
map.init() | |
canvas = d3.select(settings.element) | |
.append("canvas") | |
context = canvas.node().getContext("2d") | |
area = 1 / settings.projection.scale() / settings.ratio / 20 | |
canvas.attr("width", settings.width * settings.ratio) | |
canvas.attr("height", settings.height * settings.ratio) | |
canvas.style("width", settings.width + "px") | |
canvas.style("height", settings.height + "px") | |
canvas.style("display", "none") | |
context.lineJoin = "round" | |
context.lineCap = "round" | |
dataPath.context(context) | |
imageCache.addImage({ | |
image: settings.background, | |
scale: settings.scale, | |
translate: settings.translate | |
}) | |
} | |
this.paint = function() { | |
map.paint() | |
} | |
function scaleZoom(scale, translate) { | |
if (busy) { | |
return | |
} | |
busy = true | |
if (nearEqual(scale, settings.scale) && | |
nearEqual(translate[0], settings.translate[0]) && | |
nearEqual(translate[1], settings.translate[1])) { | |
scale = 1 | |
translate = [0, 0] | |
} | |
if (scale == 1 && settings.scale == 1 && | |
!translate[0] && !translate[1] && | |
!settings.translate[0] && !settings.translate[1]) { | |
return | |
} | |
area = 1 / settings.projection.scale() / scale / settings.ratio / 20 | |
context.save() | |
context.scale(scale * settings.ratio, scale * settings.ratio) | |
context.translate(translate[0], translate[1]) | |
context.clearRect(- translate[0], - translate[1], settings.width * settings.ratio, settings.height * settings.ratio) | |
var parameters = { | |
path: dataPath, | |
context: context, | |
scale: scale, | |
translate: translate, | |
width: settings.width, | |
height: settings.height, | |
map: settings.map | |
} | |
var image = imageCache.getImage({ | |
scale: scale, | |
translate: translate | |
}) | |
if (!image) { | |
var background = new Image(), | |
partialPainter = new PartialPainter(settings.data, parameters) | |
} | |
var translatedOne = translatePoint([settings.width, settings.height], scale, translate), | |
translatedTwo = translatePoint([settings.width, settings.height], settings.scale, settings.translate) | |
var bbox = [ | |
Math.min(- translate[0], - settings.translate[0]), | |
Math.min(- translate[1], - settings.translate[1]), | |
Math.max(translatedOne[0], translatedTwo[0]), | |
Math.max(translatedOne[1], translatedTwo[1]) | |
] | |
var zoomImage = imageCache.getFittingImage(bbox) | |
if (zoomImage) { | |
settings.background = zoomImage.image | |
settings.backgroundScale = zoomImage.scale | |
settings.backgroundTranslate = zoomImage.translate | |
} | |
d3.transition() | |
.duration(300) | |
.ease("linear") | |
.tween("zoom", function() { | |
var i = d3.interpolateNumber(settings.scale, scale), | |
oldTranslate = settings.translate, | |
oldScale = settings.scale | |
return function(t) { | |
settings.scale = i(t) | |
var newTranslate = [ | |
oldTranslate[0] + (translate[0] - oldTranslate[0]) / (scale - oldScale) * (i(t) - oldScale) * scale / i(t), | |
oldTranslate[1] + (translate[1] - oldTranslate[1]) / (scale - oldScale) * (i(t) - oldScale) * scale / i(t), | |
] | |
settings.translate = newTranslate | |
map.paint() | |
!image && partialPainter.renderNext() | |
} | |
}) | |
.each("end", function() { | |
settings.scale = scale | |
settings.translate = translate | |
if (image) { | |
context.restore() | |
settings.background = image.image | |
settings.backgroundScale = image.scale | |
settings.backgroundTranslate = image.translate | |
map.paint() | |
} else { | |
map.paint() | |
partialPainter.finish() | |
background.onload = function() { | |
context.restore() | |
imageCache.addImage({ | |
image: background, | |
scale: scale, | |
translate: translate | |
}) | |
settings.background = background | |
settings.backgroundScale = scale | |
settings.backgroundTranslate = translate | |
map.paint() | |
} | |
// TODO there is a function to get the image data from the context, is that faster? | |
// TODO use getImageData/putImageData, because it's faster? | |
background.src = canvas.node().toDataURL() | |
} | |
busy = false | |
}) | |
} | |
this.zoom = function(d) { | |
if (!d) { | |
scaleZoom.call(this, 1, [0, 0]) | |
return | |
} | |
var bounds = dataPath.bounds(d), | |
dx = bounds[1][0] - bounds[0][0], | |
dy = bounds[1][1] - bounds[0][1], | |
bx = (bounds[0][0] + bounds[1][0]) / 2, | |
by = (bounds[0][1] + bounds[1][1]) / 2, | |
scale = settings.zoomScale * | |
Math.min(settings.width / dx, settings.height / dy), | |
translate = [-bx + settings.width / scale / 2, | |
-by + settings.height / scale / 2] | |
scaleZoom.call(this, scale, translate) | |
} | |
this.settings = function(d){ | |
return map.settings() | |
} | |
} | |
}() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment