Skip to content

Instantly share code, notes, and snippets.

@dgerber
Last active May 2, 2017 22:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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
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