Clustering points into pie charts with d3-fuse. The size of each point represents how many places of worship from Open Street Map are present. Background map with an experimental tile module.
Last active
December 18, 2019 06:10
-
-
Save Andrew-Reid/cd6c23214147ddea4336b1f50238e3d6 to your computer and use it in GitHub Desktop.
D3-Fuse Clustering with pie charts
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
/*v0.0.2*/ | |
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-quadtree')) : | |
typeof define === 'function' && define.amd ? define(['exports', 'd3-quadtree'], factory) : | |
(factory((global.d3 = global.d3 || {}),global.d3)); }(this, (function (exports,d3Quadtree) { 'use strict'; | |
var c = function(f) { return (typeof f == "function") ? f : (function() { return f; }) } | |
var fuse = function(n) { | |
var nodes = n || [], padding = 0, pi = Math.PI; | |
var x = function(d) { return d.x; }, | |
y = function(d) { return d.y; }, | |
r = function(d) { return d.r; }, | |
a = function(d) { return r(d) * r(d) * Math.PI; } | |
function fuse() { initializeNodes(), step(); return nodes; } | |
function cluster() { | |
var tree = d3Quadtree.quadtree(nodes, function(d) { return d.layout.x; }, function(d) { return d.layout.y; }).visitAfter(prepare); | |
var n0; // Current Node, n1 = comparison node. | |
var count = 0; // Number of merges for a given cycle | |
for (var i = 0; i < nodes.length; ++i) n0 = nodes[i], tree.visit(apply); | |
function apply(qn, x0, y0, x1, y1) { | |
var n1 = qn.data; | |
var r = qn.r + n0.layout.r; | |
if (n1 && n1.index > n0.index && n1.layout.a && n0.layout.a) { | |
var x = n0.layout.x - n1.layout.x || 1e-6; | |
var y = n0.layout.y - n1.layout.y || 1e-6; | |
var l = Math.sqrt(x * x + y * y); | |
if (l < r + padding) { // If merge required | |
l = (r - l) / l; | |
count++; | |
// Merge logic | |
var a,b; | |
if(n1.layout.a > n0.layout.a) a = n1, b = n0; // Node1 absorbs Node0 | |
else a = n0, b = n1; // Node0 absorbs Node1 | |
// Merge nodes: | |
a.layout.x = (a.layout.x * a.layout.a + b.layout.x * b.layout.a)/(a.layout.a + b.layout.a); | |
a.layout.y = (a.layout.y * a.layout.a + b.layout.y * b.layout.a)/(b.layout.a + a.layout.a); | |
a.layout.count += b.layout.count; | |
a.layout.a += b.layout.a; | |
a.layout.r = Math.sqrt(a.layout.a/pi); | |
b.layout.r = b.layout.a = 0; | |
a.layout.children.push(b), b.layout.parent = a; | |
} | |
return; | |
} | |
return x0 > n0.layout.x + r || x1 < n0.layout.x - r || y0 > n0.layout.y + r || y1 < n0.layout.y - r; | |
} | |
return count; | |
} | |
function prepare(n) { | |
if (n.data) return n.r = n.data.layout.r; | |
for (var i = n.r = 0; i < 4; ++i) { | |
if (n[i] && n[i].r > n.r) { | |
n.r = n[i].r; | |
} | |
} | |
} | |
function step() { if(cluster()) step(); } | |
function initializeNodes() { | |
for (var i = 0, n = nodes.length, node; i < n; ++i) { | |
node = nodes[i], node.index = i; | |
node.layout = { x:x(node), y:y(node), a: a(node), r: r(node), count: 1, children: [], parent: {} } | |
} | |
} | |
fuse.nodes = function(_) { return arguments.length ? (nodes = _, fuse) : nodes; } | |
fuse.padding = function(_) { return arguments.length ? (padding = _, fuse) : padding; } | |
fuse.radius = function(_) { return arguments.length ? (r = c(_), fuse) : r; } | |
fuse.area = function(_) { return arguments.length ? (a = c(_), fuse) : a; } | |
fuse.x = function(_) { x = c(_); return fuse; } | |
fuse.y = function(_) { y = c(_); return fuse; } | |
fuse.defuse = function() { nodes.forEach(function(n) { delete n.layout; }); return fuse; } | |
fuse.step = function() { initializeNodes(); cluster(); return fuse; } | |
fuse.fuse = function() { fuse(); return fuse; } | |
return fuse; | |
}; | |
exports.fuse = fuse; | |
Object.defineProperty(exports, '__esModule', { value: true }); | |
}))); |
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
// Andrew Reid 2018 | |
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : | |
typeof define === 'function' && define.amd ? define(['exports'], factory) : | |
(factory((global.d3 = global.d3 || {}),global.d3)); | |
}(this, function (exports) { 'use strict'; | |
function geoTile() { | |
// Basic Constants | |
const tau = Math.PI * 2; | |
var lim = 85.05113; | |
var tileSize = 256; | |
// Map Properties: | |
var w = 960; | |
var h = 500; | |
// Projection Values: | |
var pk = w/tau; // projection scale k | |
var pc = [0,0] // projection geographic center | |
var pr = 0 // central longitude for rotation. | |
// Zoom Transform Values: | |
var tk = 1; // zoom transform scale k | |
var tx = w/2; // zoom transform translate x | |
var ty = h/2; // zoom transform translate y | |
// Offsets for projections where the north west limit is no in the top left. | |
var ox = function() { return 0 }; | |
var oy = function() { return 0 }; | |
// The Projection: | |
var p = d3.geoMercator() | |
.scale(pk) | |
.center(pc); | |
// Tile wrapping and zoom limits: | |
var z0 = 4; | |
var z1 = 13; | |
var extent = function() { return {left:-179.99999,top:lim,right:179.9999,bottom:-lim}; }; | |
var wrap = true; | |
// Tile ordering | |
var xyz = true; | |
// Tile source & attribution | |
var source = function(d) { | |
return "http://" + "abc"[d.y % 3] + ".tile.openstreetmap.org/" + d.z + "/" + d.x + "/" + d.y + ".png"; | |
} | |
var a = "Tiles © OpenStreetMap contributors"; | |
// Tile ordering: | |
geoTile.xyz = function(_) { | |
return arguments.length ? (xyz = _, geoTile): xyz; | |
} | |
function geoTile(_) { | |
return p(_); | |
} | |
// General Methods | |
geoTile.width = function(_) { | |
return arguments.length ? (w = _, geoTile) : w; | |
} | |
geoTile.height = function(_) { | |
return arguments.length ? (h = _, geoTile) : h; | |
} | |
geoTile.size = function(_) { | |
if(arguments.length) { | |
(_ instanceof d3.selection) ? (w = _.attr("width"), h = _.attr("height"), tx = w/2, ty = h/2) : (w = _[0], h = _[1]); | |
return geoTile; | |
} | |
else return [w,h] | |
} | |
geoTile.source = function(_) { | |
return arguments.length ? (source = _, geoTile) : source; | |
} | |
geoTile.projection = function() { | |
return p; | |
} | |
geoTile.attribution = function(_) { | |
return arguments.length ? (a = _, geoTile) : a; | |
} | |
geoTile.wrap = function(_) { | |
return arguments.length ? (wrap = _, geoTile) : wrap; | |
} | |
// Projection methods: | |
geoTile.invert = function(_) { | |
return p.invert(_); | |
} | |
geoTile.center = function(_) { | |
// Need to account for any rotation (divergence from d3.geoProjection typical behavior // | |
var rotate = d3.geoRotation(p.rotate()) | |
if(arguments.length) { | |
pc = rotate(_); p.center(pc); return geoTile; | |
} | |
else { | |
return rotate.invert(pc); | |
} | |
//return arguments.length ? (/*_[0] -= p.rotate()[0], _[1] -= p.rotate()[1], */ pc = _, p.center(pc), geoTile): pc; | |
} | |
geoTile.scale = function(_) { | |
return arguments.length ? (pk = _, p.scale(pk), geoTile) : pk; | |
} | |
geoTile.rotate = function(_) { | |
return arguments.length ? (pr = _, p.rotate([pr,0]), geoTile) : pr; | |
} | |
geoTile.fit = function(_) { | |
return arguments.length ? (p.fitSize([w,h],_),tx = p.translate()[0],ty = p.translate()[1],pk = p.scale(), geoTile) : "n/a"; | |
} | |
geoTile.fitMargin = function(m,f) { | |
return arguments.length > 1 ? (p.fitExtent([[m,m],[w-m,h-m]],f), tx = p.translate()[0],ty = p.translate()[1],pk = p.scale(), geoTile) : "n/a"; | |
} | |
geoTile.offset = function(_) { | |
return arguments.length ? (ox = _[0], oy = _[1], geoTile): [ox,oy]; | |
} | |
// Zoom Methods: | |
geoTile.zoomScale = function(_) { | |
return arguments.length ? (tk = _, p.scale(pk*tk), geoTile) : tk; | |
} | |
geoTile.zoomTranslate = function(_) { | |
return arguments.length ? (tx = _[0], ty = _[1], p.translate([tx, ty]), geoTile): [tx,ty] | |
} | |
geoTile.zoomIdentity = function() { | |
return d3.zoomIdentity.translate(tx,ty).scale(tk).translate(0,0); | |
} | |
geoTile.zoomTransform = function(t) { | |
tx = t.x, ty = t.y, tk = t.k; p.translate([tx,ty]); p.scale(pk*tk); return geoTile; | |
} | |
// Get scale factor for tile depth: | |
geoTile.tileDepth = function(z) { | |
if(arguments.length) { | |
return Math.pow(Math.E, ((z + 8) * Math.LN2)) / pk / tau; | |
} | |
else { | |
var size = pk * tk * tau; | |
var z = Math.max(Math.log(size) / Math.LN2 - 8, 0); | |
return Math.round(z); | |
} | |
} | |
// Set tile depth: | |
geoTile.setTileDepth = function(_) { | |
pk *= geoTile.tileDepth(_); p.scale(pk); return geoTile; | |
} | |
// Set/get tile size: | |
geoTile.tileSize = function(_) { | |
return arguments.length ? (tileSize = _, geoTile) : tileSize; | |
} | |
// Zoom extent methods: | |
geoTile.zoomScaleExtent = function(_) { | |
if (arguments.length) { | |
z0 = _[0]; | |
z1 = _[1]; | |
return geoTile; | |
} | |
else { | |
var size = pk * tk * tau; | |
var z = Math.max(Math.log(size) / Math.LN2 - 8, 0); | |
var max = Math.pow(2,z1)/Math.pow(2,z); | |
var min = Math.pow(2,z0)/Math.pow(2,z); | |
return [min,max]; | |
} | |
} | |
geoTile.zoomTranslateExtent = function(_) { | |
var e = extent(); | |
if (arguments.length) { | |
e.left = _[0][0]; | |
e.top = _[0][1]; | |
e.right = _[1][0]; | |
e.bottom = _[1][1]; | |
return geoTile; | |
} | |
else { | |
var x0 = p([e.left-pr,e.top])[0] - tx; | |
var y0 = p([e.left-pr,e.top])[1] - ty; | |
var x1 = p([e.right-pr,e.bottom])[0] - tx; | |
var y1 = p([e.right-pr,e.bottom])[1] - ty; | |
return [[x0,y0],[x1,y1]]; | |
} | |
} | |
geoTile.zoomTranslateConstrain = function() { | |
var e = extent(); | |
e.left = p.invert([0,0])[0]; | |
e.top = p.invert([0,0])[1]; | |
e.right = p.invert([w,h])[0]; | |
e.bottom = p.invert([w,h])[1]; | |
var x0 = p([e.left-pr,e.top])[0] - tx; | |
var y0 = p([e.left-pr,e.top])[1] - ty; | |
var x1 = p([e.right-pr,e.bottom])[0] - tx; | |
var y1 = p([e.right-pr,e.bottom])[1] - ty; | |
return [[x0,y0],[x1,y1]]; | |
} | |
// Tile Methods: | |
// Calculate Tiles: | |
geoTile.tiles = function() { | |
var size = pk * tk * tau; | |
var z = Math.max(Math.log(size) / Math.LN2 - Math.log(tileSize)/Math.log(2), 0); // tile depth | |
var s = Math.pow(2, z - Math.round(z) + 8); | |
var y0 = p([-180,lim])[1] - oy.call(this,w,h) * tk * pk/w*tau; | |
var x0 = p([-180,lim])[0] - ox.call(this,w,h) * tk * pk/w*tau; | |
var set = []; | |
var cStart = wrap ? Math.floor((0 - x0) / s) : Math.max(0, Math.floor((0 - x0) / s)); | |
var cEnd = Math.max(0, Math.ceil((w - x0) / s)); | |
var rStart = Math.max(0,Math.floor((0 - y0) / s)); | |
var rEnd = Math.max(0, Math.ceil((h - y0) / s)); | |
for(var i = cStart; i < cEnd; i++) { | |
for(var j = rStart; j < rEnd; j++) { | |
var x = i; | |
if (wrap) { | |
var k = Math.pow(2,Math.round(z)); | |
x = (i+k)%k; | |
} | |
set.push({x:x,y:j,z:Math.round(z),tx:i,ty:j, id:i+"-"+j+"-"+z}) | |
} | |
} | |
if(!xyz) { | |
set.forEach(function(d) { | |
d.y = (Math.pow(2, d.z) - d.y - 1) | |
}) | |
} | |
set.translate = [x0 / s, y0 / s]; | |
set.scale = s; | |
return set; | |
} | |
// Assign Tiles to a Selection: | |
geoTile.tile = function(g) { | |
var set = geoTile.tiles(); | |
var images = g.attr("transform", stringify(set.scale, set.translate)) | |
.selectAll("image") | |
.data(set, function(d) { return d.id; }) | |
images.exit().remove(); | |
images.enter().append("image").merge(images) | |
.attr("xlink:href", source ) | |
.attr("x", function(d) { return d.tx * tileSize; }) | |
.attr("y", function(d) { return d.ty * tileSize; }) | |
.attr("width", tileSize) | |
.attr("height", tileSize); | |
} | |
// Draw on a canvas: | |
geoTile.canvas = function(context) { | |
var set = geoTile.tiles(); | |
var k = set.scale / tileSize, r = set.scale % 1 ? Number : Math.round; | |
var ox = r(set.translate[0] * set.scale); | |
var oy = r(set.translate[1] * set.scale); | |
set.forEach(function(d) { | |
var tile = new Image(); | |
tile.src = source(d); // can also be a remote URL e.g. http:// | |
tile.onload = function() { | |
context.drawImage(tile,d.tx*tileSize*k+ox,d.ty*tileSize*k+oy,tileSize*k,tileSize*k); | |
}; | |
}) | |
} | |
// Helper stringify | |
function stringify(scale, translate) { | |
var k = scale / tileSize, r = scale % 1 ? Number : Math.round; | |
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")"; | |
} | |
geoTile.tileSet = function(_) { | |
if(arguments.length) { | |
a = _.attribution ? _.attribution : "Unknown"; | |
p = _.projection ? _.projection.scale(960/tau).translate([0,0]) : d3.geoMercator().scale(960/tau).translate([0,0]); | |
source = _.source ? _.source : (console.log("no source provided, using osm"), a = "Tiles © OpenStreetMap contributors", function(d) { return "http://" + "abc"[d.y % 3] + ".tile.openstreetmap.org/" + d.z + "/" + d.x + "/" + d.y + ".png"; }) | |
lim = _.limit ? _.limit : 85.05113; | |
tileSize = _.tileSize ? _.tileSize : 256; | |
ox = _.offsetX ? _.offsetX : function() { return 0 }; | |
oy = _.offsetY ? _.offsetY : function() { return 0 }; | |
z0 = _.minDepth ? _.minDepth : 1; | |
z1 = _.maxDepth ? _.maxDepth : 13; | |
wrap = _.wrap ? _.wrap : false; | |
xyz = _.xyz ? _.xyz : true; // tile ordering | |
} | |
return geoTile; | |
} | |
return geoTile; | |
} | |
var tileSets = { | |
CartoDB_Positron : { | |
type:"tileset", | |
attribution: "© OpenStreetMap © CartoDB", | |
source: function(d) { return "https://cartodb-basemaps-b.global.ssl.fastly.net/light_all/"+d.z+"/"+d.x+"/"+d.y+".png"; } | |
}, | |
CartoDB_PositronNoLabels : { | |
type:"tileset", | |
attribution: "© OpenStreetMap © CartoDB", | |
source: function(d) { return "https://cartodb-basemaps-b.global.ssl.fastly.net/light_nolabels/"+d.z+"/"+d.x+"/"+d.y+".png"; } | |
}, | |
CartoDB_PositronOnlyLabels : { | |
type: "tileset", | |
attribution: "© OpenStreetMap © CartoDB", | |
source: function(d) { return "https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/"+d.z+"/"+d.x+"/"+d.y+".png"; } | |
}, | |
CartoDB_DarkMatter : { | |
type: "tileset", | |
attribution: "© OpenStreetMap © CartoDB", | |
source: function(d) { return "https://cartodb-basemaps-b.global.ssl.fastly.net/dark_all/"+d.z+"/"+d.x+"/"+d.y+".png"; } | |
}, | |
CartoDB_DarkMatterNoLabels : { | |
type: "tileset", | |
attribution: "© OpenStreetMap © CartoDB", | |
source: function(d) {return "https://cartodb-basemaps-b.global.ssl.fastly.net/dark_nolabels/"+d.z+"/"+d.x+"/"+d.y+".png"; } | |
}, | |
CartoDB_DarkMatterOnlyLabels : { | |
type: "tileset", | |
attribution: "© OpenStreetMap © CartoDB", | |
source: function(d) { return "https://cartodb-basemaps-b.global.ssl.fastly.net/dark_only_labels/"+d.z+"/"+d.x+"/"+d.y+".png"; } | |
}, | |
CartoDB_Voyager : { | |
type: "tileset", | |
attribution: "© OpenStreetMap © CartoDB", | |
source: function(d) { return "https://cartodb-basemaps-b.global.ssl.fastly.net/rastertiles/voyager/"+d.z+"/"+d.x+"/"+d.y+".png";} | |
}, | |
ESRI_WorldTerrain : { | |
type: "tilset", | |
attribution: "Tiles © Esri - Source: USGS, Esri, TANA, DeLorme, and NPS", | |
source: function(d) { return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";} | |
}, | |
ESRI_WorldShadedRelief : { | |
type: "tileset", | |
attribution: "Tiles © Esri - Source: Esri", | |
source: function(d) {return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png"; } | |
}, | |
ESRI_WorldPhysical : { | |
type:"tileset", | |
attribution: "Tiles © Esri - Source: US National Park Service", | |
source: function(d) { return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Physical_Map/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png"; } | |
}, | |
ESRI_WorldStreetMap : { | |
type:"tileset", | |
attribution:"Tiles © Esri - Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom", | |
source: function(d) { return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png"; } | |
}, | |
ESRI_WorldTopoMap : { | |
type: "tileset", | |
attribution: "Tiles © Esri - Source: Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community", | |
source: function(d) { return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png"; } | |
}, | |
ESRI_WorldImagery : { | |
type:"tileset", | |
attribution: "Tiles © Esri - Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community", | |
source : function(d) { return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png"; } | |
}, | |
ESRI_OceanBasemap : { | |
type: "tileset", | |
attribution:"Tiles © Esri - Source: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri", | |
source: function(d) { return "https://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png"; } | |
}, | |
ESRI_NGWorld : { | |
type: "tileset", | |
attribution: "Tiles © Esri - Source: National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC", | |
source : function(d) { return "https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png"; } | |
}, | |
ESRI_Gray : { | |
type: "tileset", | |
attribution: "Tiles © Esri - Source: Esri, DeLorme, NAVTEQ", | |
source : function(d) { return "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png"; } | |
}, | |
OSM_Topo : { | |
type: "tileset", | |
attribution: "Tiles © OpenStreetMap contributors", | |
source: function(d) { return "https://tile.opentopomap.org/"+d.z+"/"+d.x+"/"+d.y+".png"; } | |
}, | |
OSM: { | |
type:"tileset", | |
attribution: "Tiles © OpenStreetMap contributors", | |
source: function(d) { return "https://" + "abc"[d.y % 3] + ".tile.openstreetmap.org/" + d.z + "/" + d.x + "/" + d.y + ".png"; } | |
}, | |
Stamen_Toner : { | |
type: "tileset", | |
attribution: "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.", | |
source: function(d) { return "https://stamen-tiles.a.ssl.fastly.net/toner/" + d.z + "/" + d.x + "/" + d.y + ".png"; } | |
}, | |
Stamen_TonerBackground : { | |
type:"tileset", | |
attribution: "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.", | |
source: function(d) { return "https://stamen-tiles.a.ssl.fastly.net/toner-background/" + d.z + "/" + d.x + "/" + d.y + ".png"; } | |
}, | |
Stamen_TonerLines : { | |
type:"tileset", | |
attribution:"Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.", | |
source: function(d) { return "https://stamen-tiles.a.ssl.fastly.net/toner-lines/" + d.z + "/" + d.x + "/" + d.y + ".png"; } | |
}, | |
Stamen_TonerLite : { | |
type:"tileset", | |
attribution: "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.", | |
source: function(d) { return "https://stamen-tiles.a.ssl.fastly.net/toner-lite/" + d.z + "/" + d.x + "/" + d.y + ".png"; } | |
}, | |
Stamen_Terrain : { | |
type:"tileset", | |
attribution: "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.", | |
source: function(d) { return "https://stamen-tiles.a.ssl.fastly.net/terrain/" + d.z + "/" + d.x + "/" + d.y + ".png"; } | |
}, | |
Stamen_TerrainBackground : { | |
type:"tileset", | |
attribution: "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.", | |
source: function(d) { return "https://stamen-tiles.a.ssl.fastly.net/terrain-background/" + d.z + "/" + d.x + "/" + d.y + ".png"; } | |
}, | |
Stamen_TerrainLines : { | |
type:"tileset", | |
attribution: "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.", | |
source: function(d) { return "https://stamen-tiles.a.ssl.fastly.net/terrain-lines/" + d.z + "/" + d.x + "/" + d.y + ".png"; } | |
}, | |
Stamen_Watercolor: { | |
type:"tileset", | |
attribution:"Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA.", | |
source: function(d) {return "https://stamen-tiles.a.ssl.fastly.net/watercolor/" + d.z + "/" + d.x + "/" + d.y + ".png"; } | |
} | |
} | |
// Tilesets: | |
exports.tileSet = tileSets; | |
exports.geoSlippy = geoTile; | |
Object.defineProperty(exports, '__esModule', { value: true }); | |
})); |
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"> | |
<style> | |
svg, canvas { | |
position: absolute; | |
top: 0px; | |
left: 0px; | |
} | |
</style> | |
<svg width="960" height="500"></svg> | |
<canvas width="960" height="500"></canvas> | |
<script src="https://d3js.org/d3.v5.min.js"></script> | |
<script src="d3-fuse.js"></script> | |
<script src="d3-slippy.js"></script> | |
<script> | |
// Canvas and SVG: | |
var canvas = d3.select("canvas"); | |
var ctx = canvas.node().getContext("2d"); | |
var svg = d3.select("svg"); | |
// Basic parameters: | |
var width = +canvas.attr("width"); | |
var height = +canvas.attr("height"); | |
var center = [-0.2164171,51.5050712]; | |
var colors = ["steelblue","#b2df8a","#fb9a99","#ccc","#33a02c","#e31a1c"] | |
var religions = ["christian","jewish","muslim","sikh","hindu","buddhist"]; | |
var slippy = d3.geoSlippy() | |
.size(svg) | |
.center(center) | |
.scale(700000/Math.PI/2) | |
// Create a g for the tiles: | |
var raster = svg.append("g"); | |
d3.json("LondonReligions.geojson").then(function(geojson) { | |
console.log(geojson); | |
// Set up nodes: | |
var data = geojson.features.map(function(feature) { | |
return { "lat" : feature.geometry.coordinates[1] , "long" : feature.geometry.coordinates[0], religion: feature.properties.religion } | |
}) | |
var religions = []; | |
data.forEach(function(d) { | |
if(religions.indexOf(d) == -1) religions.push(d); | |
}) | |
data.forEach(place); | |
// Set up cluster | |
var cluster = d3.fuse() | |
.nodes(data) | |
.fuse(); | |
raster.call(slippy.tile); | |
data.forEach(tally); | |
draw(data); | |
}) | |
// Helper functions: | |
function place(node) { | |
var p = slippy([+node.long,+node.lat]); | |
node.x = p[0]; | |
node.y = p[1]; | |
node.r = 8; | |
node.a = Math.PI * node.r * node.r; | |
node.count = 1; | |
} | |
// Drawing functions: | |
function draw(nodes) { | |
ctx.clearRect(0,0,width,height); | |
nodes.filter(function(d) { return d.layout.r; }).forEach(drawCircle); | |
nodes.filter(function(d) { return d.layout.r > 20; }).forEach(drawText); | |
attribute(); | |
} | |
function drawCircle(d) { | |
ctx.setTransform(1, 0, 0, 1, 0, 0); | |
var slices = d.totals; | |
var radius = d.layout.r; | |
var pie = d3.pie() | |
.sort(null) | |
.value(function(d) { return d; }); | |
var arc = d3.arc() | |
.outerRadius(radius) | |
.innerRadius(0) | |
.context(ctx); | |
ctx.translate(d.layout.x, d.layout.y); | |
var arcs = pie(slices); | |
arcs.forEach(function(d, i) { | |
ctx.beginPath(); | |
arc(d); | |
ctx.fillStyle = colors[i]; | |
ctx.fill(); | |
}); | |
} | |
function drawText(d) { | |
d = d.layout; | |
ctx.font = d.r / 3 + "px Arial"; | |
ctx.textAlign = "center"; | |
ctx.fillStyle = "white"; | |
ctx.fillText(d.count,d.x,d.y+d.r/9); | |
} | |
function attribute() { | |
ctx.font = "10px Arial"; | |
ctx.textAlign = "left"; | |
ctx.fillStyle = "black"; | |
ctx.fillText("Tiles \u00A9 Esri - Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri", 4, height-4); | |
} | |
// Tally children by religion: | |
function tally(node) { | |
var counts = [0,0,0,0,0,0]; | |
religions.forEach(function(r,i) { | |
if(r == node.religion) counts[i]++; | |
}) | |
if(node.layout.children.length > 0) { | |
node.layout.children.forEach(function(child) { | |
religions.forEach(function(r,i) { | |
if(r == child.religion) counts[i]++; | |
}) | |
}) | |
} | |
node.totals = counts; | |
} | |
// Add legend: | |
var legend = svg.append("g") | |
.selectAll("rect") | |
.data(religions) | |
.enter() | |
.append("g") | |
.attr("transform",function(d,i) { return "translate("+[5,i*30+30]+")"; }); | |
legend.append("rect") | |
.attr("width", 20) | |
.attr("height", 20) | |
.attr("fill",function(d,i) { | |
console.log(colors[i]); | |
return colors[i]; | |
}); | |
legend.append("text") | |
.attr("dx", 25) | |
.attr("dy", 15) | |
.attr("font-size","14") | |
.text(function(d) { return d.charAt(0).toUpperCase() + d.substring(1); }) | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment