Skip to content

Instantly share code, notes, and snippets.

@dgerber
Last active May 2, 2017 22:00
Show Gist options
  • Save dgerber/6185526 to your computer and use it in GitHub Desktop.
Save dgerber/6185526 to your computer and use it in GitHub Desktop.
Reusable Non-Contiguous Cartogram
(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);
<!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>
swiss-cantons.topo.json: lib/swiss-maps/topojson/swiss-cantons.json
topojson --properties --id-property abbr \
-o $@ \
lib/swiss-maps/topojson/swiss-cantons.json
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 }
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
#!/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)
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