Reusable non-contiguous cartogram, with a force layout separating features towards their ideal positions rather than actually colliding boxes. It might be useful for label placement as well.
Last active
May 2, 2017 22:00
-
-
Save dgerber/6185526 to your computer and use it in GitHub Desktop.
Reusable Non-Contiguous Cartogram
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(d3c){ | |
"use strict"; | |
// Source file core.js: | |
d3c.configurable = d3c_configurable; | |
function d3c_configurable(support, cfg){ | |
var event = d3.dispatch('change'); | |
cfg = cfg || {}; | |
config.accessor = function(key){ | |
return function(){ | |
return config.apply(support, Array.prototype.concat.call(key, Array.prototype.slice.call(arguments,0))); | |
}; | |
}; | |
for (var key in cfg){ | |
support[key] = config.accessor(key); | |
} | |
return d3.rebind(config, event, 'on'); | |
function config(key, val){ | |
var changes = 0, | |
a = arguments.length; | |
if (a == 0) return cfg; | |
if (a == 1){ | |
if (typeof key === 'object'){ | |
for (var k in key){ | |
changes += set(k, key[k]); | |
} | |
return this; | |
} | |
return cfg[key]; | |
} | |
if (a == 2) changes += set(key, val); | |
// if (a > 2) throw 'too many arguments'; | |
if (changes) event.change(null, changes); | |
return this; | |
} | |
function set(key, val){ | |
var prev = cfg[key]; | |
cfg[key] = val; | |
if (val !== prev){ | |
event.change(key, val, prev); | |
return 1; | |
} | |
return 0; | |
} | |
}; | |
// End of source file core.js | |
// Source file force.js: | |
d3c.force = { | |
separate: d3c_force_separate | |
, log: d3c_force_log | |
}; | |
function d3c_force_separate(){ | |
var cfg = { | |
padding: 10, | |
stickyness: .1, | |
conformity: 1.0, | |
loop: 'qtree', | |
shape: 'rectangle' | |
} | |
, force = d3.layout.force() | |
.charge(0).gravity(0).friction(.85) | |
.on('start.separate', function(){ | |
var loop = cfg.loop === 'qtree' ? loop_qtree : loop_simple | |
, separate = cfg.shape === 'ellipse' ? separate_ellipses : separate_rectangles; | |
force.on('tick.separate', function(e){ | |
loop(force.nodes(), trap, separate, e.alpha); | |
}); | |
}) | |
; | |
force.initXY = function(){ | |
force.nodes().forEach(function(n){ | |
if (typeof n.x === 'undefined') n.x = n.px = n.x0; | |
if (typeof n.y === 'undefined') n.y = n.py = n.y0; | |
}); | |
return force; | |
}; | |
force.autoBBox = function(nodeSel){ | |
nodeSel.each(function(node){ | |
var r = this.getBBox(); | |
node.width = r.width; | |
node.height = r.height; | |
node.x0 = r.x + r.width / 2; | |
node.y0 = r.y + r.height / 2; | |
}); | |
return force; | |
}; | |
force.config = d3c.configurable(force, cfg); | |
return d3c_force_log(force); | |
function loop_simple(nodes, f1, f2, alpha){ | |
var k = alpha * cfg.stickyness; | |
nodes.forEach(function(a,i){ | |
f1(a, k); | |
nodes.slice(i+1).forEach(function(b){ | |
f2(a, b, alpha); | |
}); | |
}); | |
} | |
function loop_qtree(nodes, f1, f2, alpha){ | |
var k = alpha * cfg.stickyness; | |
nodes.forEach(function(a,i){ | |
f1(a, k); | |
d3.geom.quadtree(nodes) | |
.visit(function(qnode, x1, y1, x2, y2){ | |
var b = qnode.point; | |
if (b && (b !== a)){ | |
f2(a, b, alpha); | |
} | |
// prune subtree if out of reach | |
var reach = 0 | |
, ox = Math.min(a.x+a.width/2, x2) - | |
Math.max(a.x-a.width/2, x1) | |
, oy = Math.min(a.y+a.height/2, y2) - | |
Math.max(a.y-a.height/2, y1); | |
return -ox > reach && -oy > reach; | |
}); | |
}); | |
} | |
function trap(a, k){ | |
// "gravity" towards ideal positions | |
a.x += (a.x0 - a.x) * k; | |
a.y += (a.y0 - a.y) * k; | |
} | |
function move(ox, oy, a, b, alpha){ | |
var conformity = cfg.conformity; | |
// shift along the axis of ideal/target positions | |
// so boxes can cross each other rather than collide | |
// this makes the result more predictable | |
var vx0 = a.x0 - b.x0, vy0 = a.y0 - b.y0 | |
, v0 = Math.sqrt(vx0 * vx0 + vy0 * vy0) | |
, shift = Math.sqrt(ox * oy) * alpha | |
, shiftX | |
, shiftY; | |
if (v0 !== 0){ | |
vx0 /= v0; | |
vy0 /= v0; | |
} else { | |
var phi = Math.random() * 2 * Math.PI; | |
vx0 = Math.cos(phi); vy0 = Math.sin(phi); | |
} | |
if (conformity === 1){ | |
shiftX = shift * vx0; | |
shiftY = shift * vy0; | |
} else { | |
// values in [0,1[ | |
// boxes arrange more compactly side by side | |
if (ox > oy){ | |
shiftX = shift * vx0 * conformity; | |
// avoiding shiftXY << shift | |
shiftY = shift * ((vy0 > 0 ? 1 : -1) * (1 - conformity) + | |
vy0 * (1 + conformity)) / 2; | |
} else { | |
shiftY = shift * vy0 * conformity; | |
shiftX = shift * ((vx0 > 0 ? 1 : -1) * (1 - conformity) + | |
vx0 * (1 + conformity)) / 2; | |
} | |
} | |
a.x += shiftX; b.x -= shiftX; | |
a.y += shiftY; b.y -= shiftY; | |
} | |
function separate_rectangles(a, b, alpha){ | |
var padding = cfg.padding; | |
// overlap | |
var ox = padding + | |
Math.min(a.x+a.width/2, b.x+b.width/2) - | |
Math.max(a.x-a.width/2, b.x-b.width/2) | |
, oy = padding + | |
Math.min(a.y+a.height/2, b.y+b.height/2) - | |
Math.max(a.y-a.height/2, b.y-b.height/2); | |
if (ox>0 && oy>0){ | |
move(ox, oy, a, b, alpha); | |
} | |
} | |
function separate_ellipses(a, b, alpha){ | |
var padding = cfg.padding; | |
// center variables on larger ellipse (a), with b unit circle | |
if (a.width * a.height < b.width * b.height){ | |
var c = b; | |
b = a; a = c; | |
} | |
var Rx1 = (2 * padding + a.width) / b.width + 1 | |
, Ry1 = (2 * padding + a.height) / b.height + 1 | |
, X = (b.x - a.x) * 2 / b.width | |
, Y = (b.y - a.y) * 2 / b.height | |
, olap = Rx1*Rx1*Ry1*Ry1 - Ry1*Ry1*X*X - Rx1*Rx1*Y*Y; | |
if (olap > 0){ | |
// gradient | |
var gx = Ry1*Ry1*X //*2(Rx1*Rx1*Ry1*Ry1) | |
, gy = Rx1*Rx1*Y //*2(Rx1*Rx1*Ry1*Ry1) | |
, g = Math.sqrt(gx * gx + gy * gy); | |
gx = Math.abs(gx / g); | |
gy = Math.abs(gy / g); | |
// overlap dimensions | |
var idepth = olap * .5 / g | |
, iwidth = idepth > 1 ? 2 : 2 * Math.sqrt(idepth * (2-idepth)); | |
// bbox of overlap area | |
var ox = Math.max(idepth * gx, iwidth * gy) // *.89 | |
, oy = Math.max(idepth * gy, iwidth * gx) // *.89 | |
; | |
move(ox * b.width * .5, oy * b.height * .5, a, b, alpha); | |
} | |
} | |
} | |
function d3c_force_log(force){ | |
var c | |
, start; | |
force.on('start.log', function(){ c = 0; start = new Date; }) | |
.on('tick.log', function(){ c++; }) | |
.on('end.log', function(){ | |
var s = ((new Date) - start) / 1000; | |
console.log(c+' ticks in '+s+' sec. ('+(c/s).toFixed(2)+'/s)'); | |
}); | |
return force; | |
} | |
// End of source file force.js | |
// Source file form.js: | |
d3c.form = d3c_form; | |
// boiler plate for html forms | |
function d3c_form(base, target){ | |
var self = {} | |
, cfg = {} // configurable values only | |
, fields = {} // cfg and buttons | |
; | |
Array.prototype.slice.call(arguments, 2).forEach(function(x){ | |
if (typeof x.key === 'undefined') x = {key: x}; | |
if (x.type !== 'button') cfg[x.key] = x.value; | |
fields[x.key] = x; | |
}); | |
self.config = d3c.configurable(self, cfg) | |
.on('change.sync_target', sync_target) | |
.on('change.sync_form', sync_form); | |
if (target.config && target.config.on){ | |
target.config.on('change.sync_form', function(key, val){ | |
if (key !== null && key in cfg){ | |
sync_form(key, val); | |
} | |
}); | |
} | |
self.refresh = sync_form; | |
render(); | |
return self; | |
function render(){ | |
var labels = base.datum(fields) | |
.selectAll('label') | |
.data(d3.values, function(d){return d.key;}); | |
labels.enter() | |
.append('label') | |
.text(function(d){return d.key;}) | |
.each(function(d){ | |
var sel = d3.select(this); | |
if (d.type === 'button'){ | |
render_button(sel, d); | |
} else if ((d.type || typeof d.value) === 'boolean'){ | |
sel | |
.append('input') | |
.property('type', 'checkbox') | |
.property('name', d.key) | |
.property('checked', d.value) | |
.on('change', function(d){ | |
self.config(this.name, this.checked); | |
}); | |
} else if (d.options){ | |
sel | |
.append('select') | |
.property('name', d.key) | |
.on('change', function(d){ | |
var o = d.options[this.selectedIndex]; | |
self.config(d.key, o ? o.value || o : o); | |
}) | |
.selectAll('option') | |
.data(d.options) | |
.enter() | |
.append('option') | |
.text(function(o){return o ? o.name || o : o;}); | |
} else { | |
sel | |
.append('input') | |
.property('type', d.type || 'text') | |
.property('name', d.key) | |
.property('value', d.value) | |
.on('change', function(d){ | |
var val = (d.setter || Number)(this.value); | |
self.config(this.name, val); | |
}); | |
} | |
}); | |
function render_button(sel, d) { | |
var b = sel | |
.text(null) | |
.append('button') | |
.property('type', 'button') | |
.text(d.key) | |
.on('click', target[d.key]); | |
if (d.key === 'start-stop'){ | |
var mess = sel.insert('span', 'button') | |
.style('opacity', .5); | |
b.on('click', function(){ | |
if (b.text() === 'start') target.start(); | |
else target.stop(); | |
}); | |
target | |
.on('tick.button', function(e){ | |
mess.text('alpha: ' + e.alpha.toFixed(4)); | |
}) | |
.on('start.button', function(e){ | |
b.text('stop'); | |
}) | |
.on('end.button', function(e){ | |
b.text('start'); | |
}); | |
} | |
} | |
sync_form(); | |
return self; | |
} | |
function sync_form(key, val, prev){ | |
if (!arguments.length){ | |
for (key in cfg){ | |
sync_form(key, target[key]()); | |
} | |
} else if (key in cfg){ | |
cfg[key] = val; | |
if (fields[key].options) { | |
base.selectAll('select[name='+key+'] > option') | |
.property('selected', function(o){ | |
return (o === val) ? true : false; | |
}); | |
} else { | |
var i = base.select('input[name='+key+']'); | |
if (i.property('type') === 'checkbox') i.property('checked', val); | |
else i.property('value', val); | |
} | |
} | |
return self; | |
} | |
function sync_target(key, val){ | |
if (!arguments.length){ | |
for (key in cfg){ | |
if (cfg[key].type !== 'button') sync_target(key, cfg[key]); | |
} | |
} else if (key in cfg){ | |
target[key](val); | |
} | |
} | |
}// End of source file form.js | |
// Source file color.js: | |
(function(ns){ | |
ns.xyz = function(x, y, z){ | |
if (arguments.length !== 1) return new XYZ(+x, +y, +z); | |
else if (x instanceof XYZ) return new XYZ(x.X, x.Y, x.Z); | |
else return rgb_XYZ((x = d3.rgb(x)).r, x.g, x.b); | |
}; | |
ns.luv = function(l, u, v){ | |
if (arguments.length !== 1) return new Luv(+l, +u, +v); | |
else if (l instanceof Luv) return new Luv(l.l, l.u, l.v); | |
else return rgb_XYZ((l = d3.rgb(l)).r, l.g, l.b).luv(); | |
}; | |
ns.interpolateXYZ = interpolateXYZ; | |
ns.interpolateLuv = interpolateLuv; | |
var d3_Color = d3.rgb(0,0,0).constructor/*.prototype.constructor*/; | |
// Corresponds roughly to RGB brighter/darker | |
var d3_lab_K = 18; | |
// tristimulus values | |
function XYZ(X, Y, Z){ this.X = X; this.Y = Y; this.Z = Z; } | |
(XYZ.prototype = new d3_Color).constructor = XYZ; | |
// D65 standard referent // d3_lab_X d3_lab_Y d3_lab_Z | |
var D65 = new XYZ(0.950470, 1, 1.088830); | |
// CIE 1976 uniform chromaticity scale | |
XYZ.prototype.ucs_u = function(){ | |
return (this.X == 0) ? 0 : 4 * this.X / (this.X + 15 * this.Y + 3 * this.Z); | |
}; | |
XYZ.prototype.ucs_v = function(){ | |
return (this.Y == 0) ? 0 : 9 * this.Y / (this.X + 15 * this.Y + 3 * this.Z); | |
}; | |
XYZ.prototype.luv = function(){ | |
var l = 116 * d3_xyz_lab(this.Y) - 16; | |
return new Luv(l, | |
13 * l * (this.ucs_u() - D65.ucs_u()), | |
13 * l * (this.ucs_v() - D65.ucs_v())); | |
}; | |
XYZ.prototype.rgb = function(){ | |
return d3.rgb( | |
d3_xyz_rgb( 3.2404542 * this.X - 1.5371385 * this.Y - 0.4985314 * this.Z), | |
d3_xyz_rgb(-0.9692660 * this.X + 1.8760108 * this.Y + 0.0415560 * this.Z), | |
d3_xyz_rgb( 0.0556434 * this.X - 0.2040259 * this.Y + 1.0572252 * this.Z) | |
); | |
}; | |
function Luv(l, u, v){ this.l = l; this.u = u; this.v = v; } | |
(Luv.prototype = new d3_Color).constructor = Luv; | |
Luv.prototype.brighter = function(k){ | |
return new Luv(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.u, this.v); | |
}; | |
Luv.prototype.darker = function(k){ | |
return new Luv(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.u, this.v); | |
}; | |
Luv.prototype.rgb = function(){ | |
return this.XYZ().rgb(); | |
}; | |
Luv.prototype.XYZ = function(){ | |
var Y = d3_lab_xyz((this.l + 16) / 116) * D65.Y, | |
ucs_u = this.u / (13 * this.l) + D65.ucs_u(), | |
ucs_v = this.v / (13 * this.l) + D65.ucs_v(), | |
s = 9 * Y / ucs_v, | |
X = ucs_u / 4 * s, | |
Z = (s - X - 15 * Y) / 3; | |
return new XYZ(X, Y, Z); | |
}; | |
function rgb_XYZ(r, g, b){ | |
r = d3_rgb_xyz(r); | |
g = d3_rgb_xyz(g); | |
b = d3_rgb_xyz(b); | |
return new XYZ(0.4124564 * r + 0.3575761 * g + 0.1804375 * b, | |
0.2126729 * r + 0.7151522 * g + 0.0721750 * b, | |
0.0193339 * r + 0.1191920 * g + 0.9503041 * b); | |
} | |
function interpolateLuv(u, v) { | |
u = ns.luv(u); | |
v = ns.luv(v); | |
var ul = u.l, | |
uu = u.u, | |
uv = u.v, | |
dl = v.l - ul, | |
du = v.u - uu, | |
dv = v.v - uv; | |
return function(t) { | |
u.l = ul + dl * t; | |
u.u = uu + du * t; | |
u.v = uv + dv * t; | |
return u; | |
}; | |
} | |
function interpolateXYZ(a, b){ | |
a = ns.xyz(a); | |
b = ns.xyz(b); | |
var aX = a.X, | |
aY = a.Y, | |
aZ = a.Z, | |
dX = b.X - aX, | |
dY = b.Y - aY, | |
dZ = b.Z - aZ; | |
return function(t) { | |
a.X = aX + dX * t; | |
a.Y = aY + dY * t; | |
a.Z = aZ + dZ * t; | |
return a; | |
}; | |
} | |
function d3_xyz_lab(x) { // L* companding | |
return x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29; | |
} | |
function d3_lab_xyz(x) { // inverse L* companding | |
return x > 0.206893034 ? x * x * x : (x - 4 / 29) / 7.787037; | |
} | |
function d3_xyz_rgb(r) { // sRGB companding | |
return Math.round(255 * (r <= 0.00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - 0.055)); | |
} | |
function d3_rgb_xyz(r) { // inverse sRGB companding | |
return (r /= 255) <= 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4); | |
} | |
})(//d3 | |
(typeof d3c === 'undefined' ? (d3c = {color: {}}) : d3c).color || (d3c.color = {}) | |
);// End of source file color.js | |
// Source file carto.js: | |
d3c.carto = { | |
noncontiguous: d3c_carto_nc | |
}; | |
function d3c_carto_nc(base){ | |
var self = {} | |
, cfg = { | |
projection: d3.geo.mercator(), | |
shape: 'feature', | |
scale: 1, // shadows area if truthy | |
area: 1, // normalized to density * mark area | |
density: .5, | |
title: function(d){ return d.id; }, | |
fill: null, | |
opacity: null, | |
stroke: null, | |
// transitions: false, | |
gabarit: false, | |
features: undefined | |
} | |
, force = self.force = d3c.force.separate() | |
; | |
self.config = d3c.configurable(self, cfg); | |
self.config.on('change.shape', function(key, val, prev){ | |
if (key === 'shape'){ | |
if (val === 'ellipse' || val === 'circle') force.shape('ellipse'); | |
else if (val === 'rectangle' || val === 'square') force.shape('rectangle'); | |
} | |
}); | |
self.render = render; | |
return self; | |
// closure: force, base | |
function render(){ | |
var sel = base.selectAll('g.d3c-node') | |
.data(cfg.features, cfg.id || cfg.title) | |
, enter = sel.enter().append('g').attr('class', 'd3c-node') | |
.call(force.drag); | |
gabarit_enter(enter); | |
sel.exit().transition().attr('opacity', 0).remove(); | |
mark_enter(enter.append('g').attr('class', 'd3c-xy'), cfg); | |
mark_update(sel.select('g.d3c-xy'), cfg); | |
gabarit_update(sel); | |
force | |
.on('tick.render', make_tick_renderer()) | |
.on('tick.render_gabarit', | |
cfg.gabarit ? make_tick_renderer_gabarit() : null) | |
.nodes(cfg.features) | |
.initXY() | |
.start(); | |
return self; | |
} | |
// closure: base | |
function make_tick_renderer(){ | |
// avoid reselecting on tick | |
var xy = base.selectAll('g.d3c-xy'); | |
return function(e){ | |
xy | |
.attr('transform', function(n){ | |
return 'translate('+(n.x-n.x0)+','+(n.y-n.y0)+')'; | |
}); | |
}; | |
} | |
function make_tick_renderer_gabarit(){ | |
var g = base.selectAll('g.d3c-gabarit') | |
, line = g.select('line') | |
, end = g.select('circle.end') | |
, bbox = g.select('rect.bbox'); | |
return function(e){ | |
line | |
.attr('x2', function(d){ return d.x; }) | |
.attr('y2', function(d){ return d.y; }); | |
end | |
.attr('cx', function(d){ return d.x; }) | |
.attr('cy', function(d){ return d.y; }); | |
bbox | |
.attr('transform', function(n){ | |
return 'translate('+(n.x-n.x0)+','+(n.y-n.y0)+')'; | |
}); | |
}; | |
} | |
function gabarit_enter(sel){ | |
sel = sel.append('g').attr('class', 'd3c-gabarit'); | |
sel.append('circle').attr('class', 'start').attr('r', 2); | |
sel.append('line'); | |
sel.append('rect').attr('class', 'bbox'); | |
sel.append('circle').attr('class', 'end').attr('r', 2); | |
} | |
function gabarit_update(sel){ | |
sel = sel.select('g.d3c-gabarit') | |
.attr('display', cfg.gabarit ? null : 'none'); | |
if (cfg.gabarit){ | |
sel = sel.transition().duration(1000); | |
sel.select('circle.start') | |
.attr('cx', function(d) { return d.x0; }) | |
.attr('cy', function(d) { return d.y0; }); | |
sel.select('line') | |
.attr('x1', function(d) { return d.x0; }) | |
.attr('y1', function(d) { return d.y0; }) | |
sel.select('rect.bbox') | |
.attr('x', function(d){ return d.x0 - d.width / 2; }) | |
.attr('y', function(d){ return d.y0 - d.height / 2; }) | |
.attr('width', function(d){ return d.width; }) | |
.attr('height', function(d){ return d.height; }); | |
sel.select('circle.end') | |
.attr('cx', function(f){ return f.x0; }) | |
.attr('cy', function(f){ return f.y0; }); | |
} | |
} | |
// function gabarit_exit(sel){ | |
// sel.selectAll('g.d3c-gabarit').remove(); | |
// } | |
function mark_enter(sel, cfg){ | |
sel.append('path') | |
.attr('vector-effect', 'non-scaling-stroke'); | |
} | |
function mark_update(sel, cfg){ | |
var scale | |
, path = d3.geo.path().projection(cfg.projection) | |
, area; | |
if (cfg.scale){ | |
scale = d3.functor(cfg.scale); | |
if (cfg.density){ | |
area = function(f){ return scale(f) * f.raw_area;}; | |
} | |
} | |
if (area || !scale) { | |
var raw_areas = 0 | |
, target_areas = 0 | |
, R; | |
area = area || d3.functor(cfg.area); | |
sel.each(function(f){ | |
raw_areas += (f.raw_area = path.area(f) || 1); | |
target_areas += (f.target_area = area(f)); | |
}); | |
R = (cfg.density || 1) * raw_areas / target_areas; | |
scale = function(f){ | |
return Math.sqrt(R * f.target_area / f.raw_area); | |
}; | |
} | |
sel.each(function(f){ | |
f.scale = scale(f); | |
compute_bbox(f); | |
}); | |
function compute_bbox(f){ | |
var b = path.bounds(f) | |
, c = path.centroid(f); | |
f.centroid = c; // dilation centered on centroid | |
f.width = f.scale * (b[1][0] - b[0][0]); | |
f.height = f.scale * (b[1][1] - b[0][1]); | |
if (cfg.shape === 'feature'){ | |
f.x0 = c[0] + f.scale * ((b[1][0] + b[0][0]) / 2 - c[0]); | |
f.y0 = c[1] + f.scale * ((b[1][1] + b[0][1]) / 2 - c[1]); | |
} else { | |
f.x0 = c[0]; | |
f.y0 = c[1]; | |
} | |
} | |
var p = sel.select('path') | |
.attr('title', cfg.title || cfg.id); | |
var shape = d3c_carto_nc.shapes[cfg.shape] || cfg.shape; | |
p.style('fill', cfg.fill) | |
.style('fill-opacity', cfg.opacity) | |
.style('stroke', cfg.stroke) | |
.call(shape, cfg); | |
} | |
} | |
d3c_carto_nc.shapes = { | |
feature: function(sel, cfg){ | |
sel | |
.attr('d', d3.geo.path().projection(cfg.projection)) | |
.attr('transform', function(f){ | |
var c = f.centroid; | |
return 'translate('+c[0]+','+c[1] | |
+')scale('+ f.scale | |
+')translate('+-c[0]+','+-c[1]+')'; | |
}); | |
// if (cfg.transitions){ | |
// sel.transition().duration(1200)//.delay(10) | |
// .attrTween('d', d3c.svg.pathTween(..., 4)); | |
// } else { | |
// sel.attr('d', ...); | |
// } | |
}, | |
ellipse: function(sel){ | |
sel | |
.attr('d', function(f){ | |
var s = f.scale * Math.sqrt(f.raw_area / (f.width * f.height * Math.PI / 4)); | |
f.width *= s; | |
f.height *= s; | |
var a = f.width / 2, b = f.height / 2; | |
return 'M'+a+',0' | |
+'A'+a+','+b+' 0 1,1 '+-a+',0' | |
+'A'+a+','+b+' 0 1,1 '+a+',0' | |
+'Z'; | |
}) | |
.attr('transform', function(f){ | |
var c = f.centroid; | |
return 'translate('+c[0]+','+c[1]+')'; | |
}); | |
}, | |
rectangle: function(sel){ | |
sel | |
.attr('d', function(f){ | |
var s = f.scale * Math.sqrt(f.raw_area / (f.width * f.height)); | |
f.width *= s; | |
f.height *= s; | |
return 'M'+-f.width/2+','+-f.height/2 | |
+'h'+f.width+'v'+f.height+'h'+-f.width+'v'+-f.height+'Z'; | |
}) | |
.attr('transform', function(f){ | |
var c = f.centroid; | |
return 'translate('+c[0]+','+c[1]+')'; | |
}); | |
}, | |
circle: function(sel){ | |
sel | |
.attr('d', function(f){ | |
var r = f.scale * Math.sqrt(f.raw_area / Math.PI); | |
f.width = f.height = 2 * r; | |
return 'M'+r+','+'0' | |
+'A'+r+','+r+' 0 1,1 '+-r+',0' | |
+'A'+r+','+r+' 0 1,1 '+r+',0' | |
+'Z'; | |
}) | |
.attr('transform', function(f){ | |
var c = f.centroid; | |
return 'translate('+c[0]+','+c[1]+')'; | |
}); | |
}, | |
square: function(sel){ | |
sel | |
.attr('d', function(f){ | |
var r = f.scale * Math.sqrt(f.raw_area) / 2; | |
f.width = f.height = 2 * r; | |
return 'M'+-f.width/2+','+-f.height/2 | |
+'h'+f.width+'v'+f.height+'h'+-f.width+'v'+-f.height+'Z'; | |
}) | |
.attr('transform', function(f){ | |
var c = f.centroid; | |
return 'translate('+c[0]+','+c[1]+')'; | |
}); | |
} | |
}; | |
// End of source file carto.js | |
return d3c; | |
})((typeof d3c === 'undefined') ? (d3c = {}) : d3c); |
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"> | |
<title>Reusable Non-Contiguous Cartogram</title> | |
<link rel="stylesheet" href="style.css"/> | |
<body> | |
<script src="http://d3js.org/topojson.v1.min.js"></script> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="d3c.js"></script> | |
<script src="vis.js"></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
swiss-cantons.topo.json: lib/swiss-maps/topojson/swiss-cantons.json | |
topojson --properties --id-property abbr \ | |
-o $@ \ | |
lib/swiss-maps/topojson/swiss-cantons.json |
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
path { fill: steelblue; fill-opacity: .8; /* stroke: #888; stroke-width: 1.5px */ } | |
form { width: 220px; float: left } | |
form label { float: left; clear: both; text-align: right; width: 180px } | |
form input { width: 3em } | |
svg { border: 1px solid grey; position: absolute } | |
.d3c-gabarit { fill: none; stroke: grey; opacity: .5; fill-opacity: .2 } | |
rect.d3c-gabarit { fill: grey; fill-opacity: .3; stroke: none } |
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
#!/usr/bin/env python | |
import json | |
from shapely.geometry import asShape, mapping, MultiPolygon | |
def simplify(s, max_nb_points=80): | |
"Trim each feature to bbox of largest polygon, with max_nb_points." | |
if s.geom_type == 'MultiPolygon': | |
parts = list(s) | |
p0 = len(parts) | |
m = max(parts, key=lambda p: p.area) | |
e = m.envelope | |
parts = [p for p in parts | |
if p is m or (p.within(e) and p.area > m.area/200)] | |
s = m if len(parts) == 1 else MultiPolygon(parts) | |
# print p0, ' ---> ', len(parts) | |
if nb_points(s) > max_nb_points: | |
# rough heuristic | |
s = s.simplify(s.area**.5 / max_nb_points) | |
if not s.is_valid: | |
# Use the 0-buffer polygon cleaning trick | |
# http://sgillies.net/blog/1106/fiona-and-shapely-spatially-cleaning-features/ | |
s = s.buffer(0.0) | |
assert s.is_valid | |
return s | |
def nb_points(s): | |
if hasattr(s, 'geoms'): | |
return sum(nb_points(p) for p in s) | |
elif s.geom_type == 'Polygon': | |
return nb_points(s.boundary) | |
return len(s.coords) | |
def convert(infile, outfile=None, max_nb_points=80, id_property=None): | |
with open(infile) as f: | |
j = json.load(f) | |
for f in j['features']: | |
s = asShape(f['geometry']) | |
f['geometry'] = mapping(simplify(s, int(max_nb_points))) | |
if id_property: | |
f.setdefault('id', f['properties'].get(id_property)) | |
f['bbox'] = s.bounds | |
if outfile: | |
with open(outfile, 'w') as f: | |
json.dump(j, f) | |
else: | |
print json.dumps(j) | |
convert.__annotations__ = dict( | |
outfile=('Output GeoJSON file name', 'option'), | |
max_nb_points=('Maximum number of points per feature', 'option'), | |
id_property=('Name of feature property to use as id (only if id is missing)', 'option') | |
) | |
if __name__ == '__main__': | |
import plac | |
plac.call(convert) |
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 width = 720 | |
, height = 480 | |
, base = d3.select('body') | |
, svg = base.append('svg').attr('width', width).attr('height', height) | |
, form = base.append('form') | |
, carto = d3c.carto.noncontiguous(svg) | |
.projection(d3.geo.albers() | |
.center([-0.4, 46.85]) | |
.rotate([-8.59, 0]) | |
.parallels([45, 50]) | |
.scale(9000) | |
.translate([width/2, height/2])) | |
; | |
d3.json('swiss-cantons.topo.json', function(topo) { | |
var geo = topojson.feature(topo, topo.objects['swiss-cantons']); | |
carto | |
.title(function(f){ | |
return f.properties.name + ' ' + f.properties.abbr; | |
}) | |
.features(geo.features) | |
.force.size([width, height]); | |
examples[0].func(); | |
carto.render(); | |
}); | |
var examples = [ | |
{ | |
name: 'Equal area features', | |
func: function(){ | |
carto.shape('feature').density(.5).scale(null).area(1) | |
.force.padding(10).conformity(1).stickyness(.1).gravity(0.02).shape('ellipse'); | |
} | |
}, | |
{ | |
name: 'Physical geography', | |
func: function(){ | |
carto.shape('feature').density(1).scale(1).gabarit(false) | |
.force.padding(-Infinity).conformity(1).stickyness(.3).gravity(0); | |
} | |
}, | |
{ | |
name: 'Ellipses equal areas', | |
func: function(){ | |
carto.shape('ellipse').density(.4).scale(null).area(1) | |
.force.padding(10).conformity(1).stickyness(.1).gravity(.002); | |
} | |
}, | |
{ | |
name: 'Rectangles', | |
func: function(){ | |
carto.shape('rectangle').density(.7).scale(Math.random) | |
.force.padding(5).conformity(1).stickyness(.1).gravity(.001); | |
} | |
}, | |
{ | |
name: 'Random areas compact', | |
func: function(){ | |
carto.scale(null).area(Math.random).density(.5) | |
.force.padding(5).conformity(.0).stickyness(.02).gravity(.01); | |
} | |
}, | |
{ | |
name: 'Dorling-like', | |
func: function(){ | |
carto.shape('circle').density(.8).scale(null).area(Math.random) | |
.force.padding(0).conformity(.5).stickyness(.01).gravity(.002); | |
} | |
}, | |
{ | |
name: 'Demers-like', | |
func: function(){ | |
carto.shape('square').density(1).scale(Math.random) | |
.force.padding(2).conformity(1).stickyness(.02).gravity(0); | |
} | |
} | |
]; | |
form | |
.call(function(form){ | |
var f = form.append('fieldset'); | |
carto.form = d3c.form(f, carto, | |
{key: 'shape', | |
options: d3.keys(d3c.carto.noncontiguous.shapes)}, | |
'density', | |
{key: 'scale', | |
options: [null, 1, Math.random]}, | |
{key: 'area', | |
options: [1, Math.random]}, | |
{key: 'gabarit', value: true}, | |
{key: 'render', | |
type: 'button'} | |
); | |
f.insert('legend', ':first-child').text('Cartogram'); | |
}) | |
.call(function(form){ | |
var f = form.append('fieldset'); | |
f.append('legend').text('Separating force'); | |
carto.force.form = d3c.form(f, carto.force, | |
'padding', 'conformity', | |
'stickyness', //'gravity', | |
{key: 'shape', | |
options: ['rectangle', 'ellipse']}, | |
{key: 'loop', | |
options: ['simple', 'qtree']}, | |
{key: 'start-stop', type: 'button'}); | |
}) | |
.call(function(form){ | |
var f = form.append('fieldset'); | |
f.append('legend').text('Example settings'); | |
f.append('select') | |
.datum(examples) | |
.on('change', function(options){ | |
var o = options[this.selectedIndex]; | |
carto.force.stop(); o.func(); carto.force.form.refresh(); | |
carto.render(); | |
}) | |
.selectAll('option') | |
.data(function(d){return d;}) | |
.enter() | |
.append('option') | |
.text(function(d){return d.name;}) | |
.attr('title', function(d){return d.func.toString();}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment