Generate Delaunay from Voronoi.

This commit is contained in:
Michael Bostock 2010-11-07 20:57:25 -08:00
Родитель 841f354568
Коммит bcf37e4a66
6 изменённых файлов: 109 добавлений и 186 удалений

Просмотреть файл

@ -4,7 +4,7 @@
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Delaunay Triangulation</title>
<script type="text/javascript" src="../../d3.js"></script>
<script type="text/javascript" src="delaunay.js"></script>
<script type="text/javascript" src="../../lib/jit/voronoi.min.js"></script>
<style type="text/css">
@import url("../../lib/colorbrewer/colorbrewer.css");
@ -22,8 +22,8 @@ path {
var w = 960,
h = 500;
var vertices = d3.range(500).map(function() {
return [~~(w * Math.random()), ~~(h * Math.random())];
var vertices = d3.range(500).map(function(d) {
return [Math.random() * w, Math.random() * h];
});
var svg = d3.select("body")
@ -32,11 +32,12 @@ var svg = d3.select("body")
.attr("height", h)
.attr("class", "PiYG");
svg.selectAll("path")
svg.append("svg:g")
.selectAll("path")
.data(delaunay(vertices))
.enter("svg:path")
.attr("class", function(d, i) { return "q" + (i % 9) + "-9"; })
.attr("d", function(d) { return "M" + d[0] + "L" + d[1] + "L" + d[2] + "Z"; });
.attr("d", function(d) { return "M" + d.join("L") + "Z"; });
</script>
</body>

Просмотреть файл

@ -1,120 +0,0 @@
// Based on work by:
// Mike Migurski - mike.teczno.com/notes/canvas-warp.html
// Joshua Bell - travellermap.com/tmp/delaunay.htm
// Sjaak Priester - codeguru.com/cpp/data/mfc_database/misc/article.php/c8901
// Joseph O'Rourke - exaflop.org/docs/cgafaq/cga1.html
function delaunay(vertices) {
var bounds = delaunay_bounds(vertices),
boundsMap = {},
circumcircles = [delaunay_circumcircle(bounds)],
triangles = [bounds];
boundsMap[bounds[0]] = 1;
boundsMap[bounds[1]] = 1;
boundsMap[bounds[2]] = 1;
// For each vertex…
vertices.forEach(function(vertex) {
var edgesMap = {},
edges = [];
// Remove triangles whose circumcircle contains the vertex.
for (var i in triangles) {
if (circumcircles[i](vertex)) {
var triangle = triangles[i];
addEdge(triangle[0], triangle[1]);
addEdge(triangle[1], triangle[2]);
addEdge(triangle[2], triangle[0]);
delete circumcircles[i];
delete triangles[i];
}
}
// Add the specified edge. If it's already present, delete it!
function addEdge(v0, v1) {
var edge = (v0[0] > v1[0]) || (v0[0] == v1[0] && v0[1] > v1[1])
? [v1, v0]
: [v0, v1];
if (edge in edgesMap) delete edges[edgesMap[edge]];
else edgesMap[edge] = edges.push(edge) - 1;
}
// Add triangles for each remaining edge and the new vertex.
for (var i in edges) {
var edge = edges[i],
triangle = [edge[0], edge[1], vertex];
circumcircles.push(delaunay_circumcircle(triangle));
triangles.push(triangle);
}
});
// Remove triangles that share a vertex with the bounds.
// The filter also removes any triangles we deleted previously.
return triangles.filter(function(triangle) {
return !triangle.some(function(vertex) {
return vertex in boundsMap;
});
});
}
function delaunay_bounds(vertices) {
var minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity,
dx,
dy;
for (var i = 0, n = vertices.length; i < n; i++) {
var vertex = vertices[i];
if (vertex[0] < minX) minX = vertex[0];
if (vertex[0] > maxX) maxX = vertex[0];
if (vertex[1] < minY) minY = vertex[1];
if (vertex[1] > maxY) maxY = vertex[1];
}
dx = (maxX - minX) * 10;
dy = (maxY - minX) * 10;
return [
[minX - dx, minY - dy * 3],
[minX - dx, maxY + dy],
[maxX + dx * 3, maxY + dy]
];
}
function delaunay_circumcircle(triangle) {
var v0 = triangle[0],
v1 = triangle[1],
v2 = triangle[2],
A = v1[0] - v0[0],
B = v1[1] - v0[1],
C = v2[0] - v0[0],
D = v2[1] - v0[1],
E = A * (v0[0] + v1[0]) + B * (v0[1] + v1[1]),
F = C * (v0[0] + v2[0]) + D * (v0[1] + v2[1]),
G = 2 * (A * (v2[1] - v1[1]) - B * (v2[0] - v1[0])),
cx,
cy,
dx,
dy,
r2;
// If collinear, find extremes and use the midpoint.
if (Math.abs(G) < 1e-6) {
var minX = Math.min(v0[0], v1[0], v2[0]),
minY = Math.min(v0[1], v1[1], v2[1]),
maxX = Math.max(v0[0], v1[0], v2[0]),
maxY = Math.max(v0[1], v1[1], v2[1]);
dx = (cx = (minX + maxX) / 2) - minX;
dy = (cy = (minY + maxY) / 2) - minY;
} else {
dx = (cx = (D * E - B * F) / G) - v0[0];
dy = (cy = (A * F - C * E) / G) - v0[1];
}
r2 = dx * dx + dy * dy;
return function(vertex) {
var dx = cx - vertex[0],
dy = cy - vertex[1],
d2 = dx * dx + dy * dy;
return d2 <= r2;
};
}

Просмотреть файл

@ -12,6 +12,7 @@
<li><a href="calendar/dji.html">calendar-dji</a></li>
<li><a href="calendar/vix.html">calendar-vix</a></li>
<li><a href="choropleth/choropleth.html">choropleth</a></li>
<li><a href="delaunay/delaunay.html">delaunay</a></li>
<li><a href="donut/donut.html">donut</a></li>
<li><a href="dot/dot.html">dot</a></li>
<li><a href="force/force.html">force</a></li>
@ -19,9 +20,11 @@
<li><a href="line/line.html">line</a></li>
<li><a href="pie/pie.html">pie</a></li>
<li><a href="pie/pie-transition.html">pie-transition</a></li>
<li><a href="splom/splom.html">splom</a></li>
<li><a href="stream/stream.html">stream</a></li>
<li><a href="stream/stack.html">stack</a></li>
<li><a href="symbol-map/symbol-map.html">symbol-map</a></li>
<li><a href="voronoi/voronoi.html">voronoi</a></li>
<li><a href="zoom/zoom.html">zoom</a></li>
</ul>
</body>

Просмотреть файл

@ -7,42 +7,90 @@
* @returns polygons [[[x1, y1], [x2, y2], ], ]
*/
voronoi = function(vertices) {
var polygons = vertices.map(function() { return []; }),
opposite = {"l": "r", "r": "l"};
var polygons = vertices.map(function() { return []; });
// Note: we expect the caller to clip the polygons, if needed.
var Canvas = {
plotEdge: function(e) {
var s1,
s2,
x1,
x2,
y1,
y2;
if (e.a == 1 && e.b >= 0) {
s1 = e.ep["r"];
s2 = e.ep["l"];
} else {
s1 = e.ep["l"];
s2 = e.ep["r"];
}
if (e.a == 1) {
y1 = s1 ? s1.y : -1e6;
x1 = e.c - e.b * y1;
y2 = s2 ? s2.y : 1e6;
x2 = e.c - e.b * y2;
} else {
x1 = s1 ? s1.x : -1e6;
y1 = e.c - e.a * x1;
x2 = s2 ? s2.x : 1e6;
y2 = e.c - e.a * x2;
}
var v1 = [x1, y1],
v2 = [x2, y2];
polygons[e.region["l"].index].push(v1, v2);
polygons[e.region["r"].index].push(v1, v2);
voronoi_tessellate(vertices, function(e) {
var s1,
s2,
x1,
x2,
y1,
y2;
if (e.a == 1 && e.b >= 0) {
s1 = e.ep["r"];
s2 = e.ep["l"];
} else {
s1 = e.ep["l"];
s2 = e.ep["r"];
}
};
if (e.a == 1) {
y1 = s1 ? s1.y : -1e6;
x1 = e.c - e.b * y1;
y2 = s2 ? s2.y : 1e6;
x2 = e.c - e.b * y2;
} else {
x1 = s1 ? s1.x : -1e6;
y1 = e.c - e.a * x1;
x2 = s2 ? s2.x : 1e6;
y2 = e.c - e.a * x2;
}
var v1 = [x1, y1],
v2 = [x2, y2];
polygons[e.region["l"].index].push(v1, v2);
polygons[e.region["r"].index].push(v1, v2);
});
// Reconnect the polygon segments into counterclockwise loops.
return polygons.map(function(polygon, i) {
var cx = vertices[i][0],
cy = vertices[i][1];
polygon.forEach(function(v) {
v.angle = Math.atan2(v[0] - cx, v[1] - cy);
});
return polygon.sort(function(a, b) {
return a.angle - b.angle;
}).filter(function(d, i) {
return !i || (d.angle - polygon[i - 1].angle > 1e-10);
});
});
};
/**
* @param vertices [[x1, y1], [x2, y2], ]
* @returns triangles [[[x1, y1], [x2, y2], [x3, y3]], ]
*/
delaunay = function(vertices) {
var edges = vertices.map(function() { return []; }),
triangles = [];
// Use the Voronoi tessellation to determine Delaunay edges.
voronoi_tessellate(vertices, function(e) {
edges[e.region["l"].index].push(vertices[e.region["r"].index]);
});
// Reconnect the edges into counterclockwise triangles.
edges.forEach(function(edge, i) {
var v = vertices[i],
cx = v[0],
cy = v[1];
edge.forEach(function(v) {
v.angle = Math.atan2(v[0] - cx, v[1] - cy);
});
edge.sort(function(a, b) {
return a.angle - b.angle;
});
for (var j = 0, m = edge.length - 1; j < m; j++) {
triangles.push([v, edge[j], edge[j + 1]]);
}
});
return triangles;
};
var voronoi_opposite = {"l": "r", "r": "l"};
function voronoi_tessellate(vertices, callback) {
var Sites = {
list: vertices
@ -125,7 +173,7 @@ voronoi = function(vertices) {
rightRegion: function(he) {
return he.edge == null
? Sites.bottomSite
: he.edge.region[opposite[he.side]];
: he.edge.region[voronoi_opposite[he.side]];
}
};
@ -244,8 +292,8 @@ voronoi = function(vertices) {
endPoint: function(edge, side, site) {
edge.ep[side] = site;
if (!edge.ep[opposite[side]]) return;
Canvas.plotEdge(edge);
if (!edge.ep[voronoi_opposite[side]]) return;
callback(edge);
},
distance: function(s, t) {
@ -359,7 +407,7 @@ voronoi = function(vertices) {
e = Geom.bisect(bot, top);
bisector = EdgeList.createHalfEdge(e, pm);
EdgeList.insert(llbnd, bisector);
Geom.endPoint(e, opposite[pm], v);
Geom.endPoint(e, voronoi_opposite[pm], v);
p = Geom.intersect(llbnd, bisector);
if (p) {
EventQueue.del(llbnd);
@ -377,20 +425,6 @@ voronoi = function(vertices) {
for (lbnd = EdgeList.right(EdgeList.leftEnd);
lbnd != EdgeList.rightEnd;
lbnd = EdgeList.right(lbnd)) {
Canvas.plotEdge(lbnd.edge);
callback(lbnd.edge);
}
// Reconnect the polygon segments into counterclockwise loops.
return polygons.map(function(polygon, i) {
var cx = vertices[i][0],
cy = vertices[i][1];
polygon.forEach(function(v) {
v.angle = Math.atan2(v[0] - cx, v[1] - cy);
});
return polygon.sort(function(a, b) {
return a.angle - b.angle;
}).filter(function(d, i) {
return !i || (d.angle - polygon[i - 1].angle > 1e-10);
});
});
};
}

17
lib/jit/voronoi.min.js поставляемый
Просмотреть файл

@ -1,8 +1,9 @@
(function(){var k=null;
voronoi=function(w){var y=w.map(function(){return[]}),z={l:"r",r:"l"},A={A:function(a){var c,b,d,e;if(a.d==1&&a.a>=0){c=a.k.r;b=a.k.l}else{c=a.k.l;b=a.k.r}if(a.d==1){d=c?c.y:-1E6;c=a.b-a.a*d;e=b?b.y:1E6;b=a.b-a.a*e}else{c=c?c.x:-1E6;d=a.b-a.d*c;b=b?b.x:1E6;e=a.b-a.d*b}c=[c,d];b=[b,e];y[a.region.l.index].push(c,b);y[a.region.r.index].push(c,b)}},t={c:w.map(function(a,c){return{index:c,x:a[0],y:a[1]}}).sort(function(a,c){return a.y<c.y?-1:a.y>c.y?1:a.x<c.x?-1:a.x>c.x?1:0}),v:k},f={c:[],m:k,n:k,D:function(){f.m=
f.o(k,"l");f.n=f.o(k,"l");f.m.r=f.n;f.n.l=f.m;f.c.unshift(f.m,f.n)},o:function(a,c){return{g:a,h:c,u:k,l:k,r:k}},i:function(a,c){c.l=a;c.r=a.r;a.r.l=c;a.r=c},F:function(a){var c=f.m;do c=c.r;while(c!=f.n&&n.H(c,a));return c=c.l},j:function(a){a.l.r=a.r;a.r.l=a.l;a.g=k},right:function(a){return a.r},left:function(a){return a.l},G:function(a){return a.g==k?t.v:a.g.region[a.h]},B:function(a){return a.g==k?t.v:a.g.region[z[a.h]]}},n={z:function(a,c){var b={region:{l:a,r:c},k:{l:k,r:k}},d=c.x-a.x,e=c.y-
a.y,h=d>0?d:-d,l=e>0?e:-e;b.b=a.x*d+a.y*e+(d*d+e*e)*0.5;if(h>l){b.d=1;b.a=e/d;b.b/=d}else{b.a=1;b.d=d/e;b.b/=e}return b},t:function(a,c){var b=a.g,d=c.g;if(!b||!d||b.region.r==d.region.r)return k;var e=b.d*d.a-b.a*d.d;if(Math.abs(e)<1.0E-10)return k;var h=(b.b*d.a-d.b*b.a)/e;e=(d.b*b.d-b.b*d.d)/e;var l=b.region.r,s=d.region.r;if(l.y<s.y||l.y==s.y&&l.x<s.x){l=a;b=b}else{l=c;b=d}if((b=h>=b.region.r.x)&&l.h=="l"||!b&&l.h=="r")return k;return{x:h,y:e}},H:function(a,c){var b=a.g,d=b.region.r,e=c.x>d.x;
if(e&&a.h=="l")return 1;if(!e&&a.h=="r")return 0;if(b.d==1){var h=c.y-d.y,l=c.x-d.x,s=0,o=0;if(!e&&b.a<0||e&&b.a>=0)o=s=h>=b.a*l;else{o=c.x+c.y*b.a>b.b;if(b.a<0)o=!o;o||(s=1)}if(!s){d=d.x-b.region.l.x;o=b.a*(l*l-h*h)<d*h*(1+2*l/d+b.a*b.a);if(b.a<0)o=!o}}else{l=b.b-b.d*c.x;b=c.y-l;h=c.x-d.x;d=l-d.y;o=b*b>h*h+d*d}return a.h=="l"?o:!o},w:function(a,c,b){a.k[c]=b;a.k[z[c]]&&A.A(a)},s:function(a,c){var b=a.x-c.x,d=a.y-c.y;return Math.sqrt(b*b+d*d)}},i={c:[],i:function(a,c,b){a.u=c;a.p=c.y+b;b=0;for(var d=
i.c,e=d.length;b<e;b++){var h=d[b];if(!(a.p>h.p||a.p==h.p&&c.x>h.u.x))break}d.splice(b,0,a)},j:function(a){for(var c=0,b=i.c,d=b.length;c<d&&b[c]!=a;++c);b.splice(c,1)},empty:function(){return i.c.length==0},I:function(a){for(var c=0,b=i.c,d=b.length;c<d;++c)if(b[c]==a)return b[c+1];return k},min:function(){var a=i.c[0];return{x:a.u.x,y:a.p}},C:function(){return i.c.shift()}};f.D();t.v=t.c.shift();for(var p=t.c.shift(),x,g,q,v,B,j,r,m,u;;){i.empty()||(x=i.min());if(p&&(i.empty()||p.y<x.y||p.y==x.y&&
p.x<x.x)){g=f.F(p);q=f.right(g);r=f.B(g);u=n.z(r,p);j=f.o(u,"l");f.i(g,j);if(m=n.t(g,j)){i.j(g);i.i(g,m,n.s(m,p))}g=j;j=f.o(u,"r");f.i(g,j);(m=n.t(j,q))&&i.i(j,m,n.s(m,p));p=t.c.shift()}else if(i.empty())break;else{g=i.C();v=f.left(g);q=f.right(g);B=f.right(q);r=f.G(g);j=f.B(q);m=g.u;n.w(g.g,g.h,m);n.w(q.g,q.h,m);f.j(g);i.j(q);f.j(q);g="l";if(r.y>j.y){g=r;r=j;j=g;g="r"}u=n.z(r,j);j=f.o(u,g);f.i(v,j);n.w(u,z[g],m);if(m=n.t(v,j)){i.j(v);i.i(v,m,n.s(m,r))}(m=n.t(j,B))&&i.i(j,m,n.s(m,r))}}for(g=f.right(f.m);g!=
f.n;g=f.right(g))A.A(g.g);return y.map(function(a,c){var b=w[c][0],d=w[c][1];a.forEach(function(e){e.q=Math.atan2(e[0]-b,e[1]-d)});return a.sort(function(e,h){return e.q-h.q}).filter(function(e,h){return!h||e.q-a[h-1].q>1.0E-10})})};})()
(function(){var m=null;
voronoi=function(q){var v=q.map(function(){return[]});A(q,function(h){var c,g,i,j;if(h.d==1&&h.a>=0){c=h.m.r;g=h.m.l}else{c=h.m.l;g=h.m.r}if(h.d==1){i=c?c.y:-1E6;c=h.b-h.a*i;j=g?g.y:1E6;g=h.b-h.a*j}else{c=c?c.x:-1E6;i=h.b-h.d*c;g=g?g.x:1E6;j=h.b-h.d*g}c=[c,i];g=[g,j];v[h.region.l.index].push(c,g);v[h.region.r.index].push(c,g)});return v.map(function(h,c){var g=q[c][0],i=q[c][1];h.forEach(function(j){j.j=Math.atan2(j[0]-g,j[1]-i)});return h.sort(function(j,r){return j.j-r.j}).filter(function(j,r){return!r||
j.j-h[r-1].j>1.0E-10})})};delaunay=function(q){var v=q.map(function(){return[]}),h=[];A(q,function(c){v[c.region.l.index].push(q[c.region.r.index])});v.forEach(function(c,g){var i=q[g],j=i[0],r=i[1];c.forEach(function(s){s.j=Math.atan2(s[0]-j,s[1]-r)});c.sort(function(s,z){return s.j-z.j});for(var f=0,t=c.length-1;f<t;f++)h.push([i,c[f],c[f+1]])});return h};var B={l:"r",r:"l"};
function A(q,v){var h={c:q.map(function(a,d){return{index:d,x:a[0],y:a[1]}}).sort(function(a,d){return a.y<d.y?-1:a.y>d.y?1:a.x<d.x?-1:a.x>d.x?1:0}),v:m},c={c:[],n:m,o:m,C:function(){c.n=c.p(m,"l");c.o=c.p(m,"l");c.n.r=c.o;c.o.l=c.n;c.c.unshift(c.n,c.o)},p:function(a,d){return{g:a,h:d,u:m,l:m,r:m}},i:function(a,d){d.l=a;d.r=a.r;a.r.l=d;a.r=d},D:function(a){var d=c.n;do d=d.r;while(d!=c.o&&g.G(d,a));return d=d.l},k:function(a){a.l.r=a.r;a.r.l=a.l;a.g=m},right:function(a){return a.r},left:function(a){return a.l},
F:function(a){return a.g==m?h.v:a.g.region[a.h]},A:function(a){return a.g==m?h.v:a.g.region[B[a.h]]}},g={z:function(a,d){var b={region:{l:a,r:d},m:{l:m,r:m}},e=d.x-a.x,k=d.y-a.y,o=e>0?e:-e,n=k>0?k:-k;b.b=a.x*e+a.y*k+(e*e+k*k)*0.5;if(o>n){b.d=1;b.a=k/e;b.b/=e}else{b.a=1;b.d=e/k;b.b/=k}return b},t:function(a,d){var b=a.g,e=d.g;if(!b||!e||b.region.r==e.region.r)return m;var k=b.d*e.a-b.a*e.d;if(Math.abs(k)<1.0E-10)return m;var o=(b.b*e.a-e.b*b.a)/k;k=(e.b*b.d-b.b*e.d)/k;var n=b.region.r,x=e.region.r;
if(n.y<x.y||n.y==x.y&&n.x<x.x){n=a;b=b}else{n=d;b=e}if((b=o>=b.region.r.x)&&n.h=="l"||!b&&n.h=="r")return m;return{x:o,y:k}},G:function(a,d){var b=a.g,e=b.region.r,k=d.x>e.x;if(k&&a.h=="l")return 1;if(!k&&a.h=="r")return 0;if(b.d==1){var o=d.y-e.y,n=d.x-e.x,x=0,u=0;if(!k&&b.a<0||k&&b.a>=0)u=x=o>=b.a*n;else{u=d.x+d.y*b.a>b.b;if(b.a<0)u=!u;u||(x=1)}if(!x){e=e.x-b.region.l.x;u=b.a*(n*n-o*o)<e*o*(1+2*n/e+b.a*b.a);if(b.a<0)u=!u}}else{n=b.b-b.d*d.x;b=d.y-n;o=d.x-e.x;e=n-e.y;u=b*b>o*o+e*e}return a.h=="l"?
u:!u},w:function(a,d,b){a.m[d]=b;a.m[B[d]]&&v(a)},s:function(a,d){var b=a.x-d.x,e=a.y-d.y;return Math.sqrt(b*b+e*e)}},i={c:[],i:function(a,d,b){a.u=d;a.q=d.y+b;b=0;for(var e=i.c,k=e.length;b<k;b++){var o=e[b];if(!(a.q>o.q||a.q==o.q&&d.x>o.u.x))break}e.splice(b,0,a)},k:function(a){for(var d=0,b=i.c,e=b.length;d<e&&b[d]!=a;++d);b.splice(d,1)},empty:function(){return i.c.length==0},H:function(a){for(var d=0,b=i.c,e=b.length;d<e;++d)if(b[d]==a)return b[d+1];return m},min:function(){var a=i.c[0];return{x:a.u.x,
y:a.q}},B:function(){return i.c.shift()}};c.C();h.v=h.c.shift();for(var j=h.c.shift(),r,f,t,s,z,l,w,p,y;;){i.empty()||(r=i.min());if(j&&(i.empty()||j.y<r.y||j.y==r.y&&j.x<r.x)){f=c.D(j);t=c.right(f);w=c.A(f);y=g.z(w,j);l=c.p(y,"l");c.i(f,l);if(p=g.t(f,l)){i.k(f);i.i(f,p,g.s(p,j))}f=l;l=c.p(y,"r");c.i(f,l);(p=g.t(l,t))&&i.i(l,p,g.s(p,j));j=h.c.shift()}else if(i.empty())break;else{f=i.B();s=c.left(f);t=c.right(f);z=c.right(t);w=c.F(f);l=c.A(t);p=f.u;g.w(f.g,f.h,p);g.w(t.g,t.h,p);c.k(f);i.k(t);c.k(t);
f="l";if(w.y>l.y){f=w;w=l;l=f;f="r"}y=g.z(w,l);l=c.p(y,f);c.i(s,l);g.w(y,B[f],p);if(p=g.t(s,l)){i.k(s);i.i(s,p,g.s(p,w))}(p=g.t(l,z))&&i.i(l,p,g.s(p,w))}}for(f=c.right(c.n);f!=c.o;f=c.right(f))v(f.g)};})()

Просмотреть файл

@ -168,3 +168,7 @@ d3.time = {
format: 1,
parse: 1
};
// jit
var voronoi,
delaunay;