This commit is contained in:
Mike Bostock 2014-01-09 15:31:05 -08:00
Родитель aee121427d 19789b6821
Коммит d0a047ae80
6 изменённых файлов: 131 добавлений и 171 удалений

105
d3.js поставляемый
Просмотреть файл

@ -1179,6 +1179,12 @@ d3 = function() {
function d3_sgn(x) {
return x > 0 ? 1 : x < 0 ? -1 : 0;
}
function d3_isCCWTurn(a, b, c) {
return d3_cross2d(a, b, c) > 0;
}
function d3_cross2d(o, a, b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
function d3_acos(x) {
return x > 1 ? 0 : x < -1 ? π : Math.acos(x);
}
@ -3117,18 +3123,15 @@ d3 = function() {
for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) {
b = v[j];
if (a[1] <= y) {
if (b[1] > y && isLeft(a, b, p) > 0) ++wn;
if (b[1] > y && d3_isCCWTurn(a, b, p)) ++wn;
} else {
if (b[1] <= y && isLeft(a, b, p) < 0) --wn;
if (b[1] <= y && !d3_isCCWTurn(a, b, p)) --wn;
}
a = b;
}
}
return wn !== 0;
}
function isLeft(a, b, c) {
return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
}
function interpolate(from, to, direction, listener) {
var a = 0, a1 = 0;
if (from == null || (a = corner(from, direction)) !== (a1 = corner(to, direction)) || comparePoints(from, to) < 0 ^ direction > 0) {
@ -4197,65 +4200,17 @@ d3 = function() {
if (arguments.length) return hull(vertices);
function hull(data) {
if (data.length < 3) return [];
var fx = d3_functor(x), fy = d3_functor(y), n = data.length, vertices, plen = n - 1, points = [], stack = [], d, i, j, h = 0, x1, y1, x2, y2, u, v, a, sp;
if (fx === d3_geom_pointX && y === d3_geom_pointY) vertices = data; else for (i = 0,
vertices = []; i < n; ++i) {
vertices.push([ +fx.call(this, d = data[i], i), +fy.call(this, d, i) ]);
var fx = d3_functor(x), fy = d3_functor(y), i, n = data.length, points = [], flippedPoints = [];
for (i = 0; i < n; i++) {
points.push([ +fx.call(this, data[i], i), +fy.call(this, data[i], i), i ]);
}
for (i = 1; i < n; ++i) {
if (vertices[i][1] < vertices[h][1] || vertices[i][1] == vertices[h][1] && vertices[i][0] < vertices[h][0]) h = i;
}
for (i = 0; i < n; ++i) {
if (i === h) continue;
y1 = vertices[i][1] - vertices[h][1];
x1 = vertices[i][0] - vertices[h][0];
points.push({
angle: Math.atan2(y1, x1),
index: i
});
}
points.sort(function(a, b) {
return a.angle - b.angle;
});
a = points[0].angle;
v = points[0].index;
u = 0;
for (i = 1; i < plen; ++i) {
j = points[i].index;
if (a == points[i].angle) {
x1 = vertices[v][0] - vertices[h][0];
y1 = vertices[v][1] - vertices[h][1];
x2 = vertices[j][0] - vertices[h][0];
y2 = vertices[j][1] - vertices[h][1];
if (x1 * x1 + y1 * y1 >= x2 * x2 + y2 * y2) {
points[i].index = -1;
continue;
} else {
points[u].index = -1;
}
}
a = points[i].angle;
u = i;
v = j;
}
stack.push(h);
for (i = 0, j = 0; i < 2; ++j) {
if (points[j].index > -1) {
stack.push(points[j].index);
i++;
}
}
sp = stack.length;
for (;j < plen; ++j) {
if (points[j].index < 0) continue;
while (!d3_geom_hullCCW(stack[sp - 2], stack[sp - 1], points[j].index, vertices)) {
--sp;
}
stack[sp++] = points[j].index;
}
var poly = [];
for (i = sp - 1; i >= 0; --i) poly.push(data[stack[i]]);
return poly;
points.sort(d3_geom_hullOrder);
for (i = 0; i < n; i++) flippedPoints.push([ points[i][0], -points[i][1] ]);
var upper = d3_geom_hullUpper(points), lower = d3_geom_hullUpper(flippedPoints);
var skipLeft = lower[0] === upper[0], skipRight = lower[lower.length - 1] === upper[upper.length - 1], polygon = [];
for (i = upper.length - 1; i >= 0; --i) polygon.push(data[points[upper[i]][2]]);
for (i = +skipLeft; i < lower.length - skipRight; ++i) polygon.push(data[points[lower[i]][2]]);
return polygon;
}
hull.x = function(_) {
return arguments.length ? (x = _, hull) : x;
@ -4265,18 +4220,18 @@ d3 = function() {
};
return hull;
};
function d3_geom_hullCCW(i1, i2, i3, v) {
var t, a, b, c, d, e, f;
t = v[i1];
a = t[0];
b = t[1];
t = v[i2];
c = t[0];
d = t[1];
t = v[i3];
e = t[0];
f = t[1];
return (f - b) * (c - a) - (d - b) * (e - a) > 0;
function d3_geom_hullUpper(points) {
var n = points.length, hull = [ 0, 1 ], hs = 2;
for (var i = 2; i < n; i++) {
while (hs > 1 && !d3_isCCWTurn(points[hull[hs - 2]], points[hull[hs - 1]], points[i])) {
hs--;
}
hull[hs++] = i;
}
return hull.slice(0, hs);
}
function d3_geom_hullOrder(a, b) {
return a[0] - b[0] || a[1] - b[1];
}
d3.geom.polygon = function(coordinates) {
d3_subclass(coordinates, d3_geom_polygonPrototype);

10
d3.min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -79,9 +79,9 @@ function d3_geo_clipExtent(x0, y0, x1, y1) {
for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) {
b = v[j];
if (a[1] <= y) {
if (b[1] > y && isLeft(a, b, p) > 0) ++wn;
if (b[1] > y && d3_isCCWTurn(a, b, p)) ++wn;
} else {
if (b[1] <= y && isLeft(a, b, p) < 0) --wn;
if (b[1] <= y && !d3_isCCWTurn(a, b, p)) --wn;
}
a = b;
}
@ -89,10 +89,6 @@ function d3_geo_clipExtent(x0, y0, x1, y1) {
return wn !== 0;
}
function isLeft(a, b, c) {
return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
}
function interpolate(from, to, direction, listener) {
var a = 0, a1 = 0;
if (from == null ||

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

@ -1,15 +1,18 @@
import "../core/functor";
import "../math/trigonometry";
import "geom";
import "point";
/**
* Computes the 2D convex hull of a set of points using Graham's scanning
* algorithm. The algorithm has been implemented as described in Cormen,
* Leiserson, and Rivest's Introduction to Algorithms. The running time of
* this algorithm is O(n log n), where n is the number of input points.
* Computes the 2D convex hull of a set of points using the monotone chain
* algorithm:
* http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain)
*
* @param vertices [[x1, y1], [x2, y2], ]
* @returns polygon [[x1, y1], [x2, y2], ]
* The runtime of this algorithm is O(n log n), where n is the number of input
* points. However in practice it outperforms other O(n log n) hulls.
*
* @param vertices [[x1, y1], [x2, y2], ...]
* @returns polygon [[x1, y1], [x2, y2], ...]
*/
d3.geom.hull = function(vertices) {
var x = d3_geom_pointX,
@ -18,86 +21,40 @@ d3.geom.hull = function(vertices) {
if (arguments.length) return hull(vertices);
function hull(data) {
// Hull of < 3 points is not well-defined
if (data.length < 3) return [];
var fx = d3_functor(x),
fy = d3_functor(y),
i,
n = data.length,
vertices, // TODO use parallel arrays
plen = n - 1,
points = [],
stack = [],
d,
i, j, h = 0, x1, y1, x2, y2, u, v, a, sp;
points = [], // of the form [[x0, y0, 0], ..., [xn, yn, n]]
flippedPoints = [];
if (fx === d3_geom_pointX && y === d3_geom_pointY) vertices = data;
else for (i = 0, vertices = []; i < n; ++i) {
vertices.push([+fx.call(this, d = data[i], i), +fy.call(this, d, i)]);
for (i = 0 ; i < n; i++) {
points.push([+fx.call(this, data[i], i), +fy.call(this, data[i], i), i]);
}
// find the starting ref point: leftmost point with the minimum y coord
for (i = 1; i < n; ++i) {
if (vertices[i][1] < vertices[h][1]
|| vertices[i][1] == vertices[h][1]
&& vertices[i][0] < vertices[h][0]) h = i;
}
// sort ascending by x-coord first, y-coord second
points.sort(d3_geom_hullOrder);
// calculate polar angles from ref point and sort
for (i = 0; i < n; ++i) {
if (i === h) continue;
y1 = vertices[i][1] - vertices[h][1];
x1 = vertices[i][0] - vertices[h][0];
points.push({angle: Math.atan2(y1, x1), index: i});
}
points.sort(function(a, b) { return a.angle - b.angle; });
// we flip bottommost points across y axis so we can use the upper hull routine on both
for (i = 0; i < n; i++) flippedPoints.push([points[i][0], -points[i][1]]);
// toss out duplicate angles
a = points[0].angle;
v = points[0].index;
u = 0;
for (i = 1; i < plen; ++i) {
j = points[i].index;
if (a == points[i].angle) {
// keep angle for point most distant from the reference
x1 = vertices[v][0] - vertices[h][0];
y1 = vertices[v][1] - vertices[h][1];
x2 = vertices[j][0] - vertices[h][0];
y2 = vertices[j][1] - vertices[h][1];
if (x1 * x1 + y1 * y1 >= x2 * x2 + y2 * y2) {
points[i].index = -1;
continue;
} else {
points[u].index = -1;
}
}
a = points[i].angle;
u = i;
v = j;
}
var upper = d3_geom_hullUpper(points),
lower = d3_geom_hullUpper(flippedPoints);
// initialize the stack
stack.push(h);
for (i = 0, j = 0; i < 2; ++j) {
if (points[j].index > -1) {
stack.push(points[j].index);
i++;
}
}
sp = stack.length;
// construct the polygon, removing possible duplicate endpoints
var skipLeft = lower[0] === upper[0],
skipRight = lower[lower.length - 1] === upper[upper.length - 1],
polygon = [];
// do graham's scan
for (; j < plen; ++j) {
if (points[j].index < 0) continue; // skip tossed out points
while (!d3_geom_hullCCW(stack[sp - 2], stack[sp - 1], points[j].index, vertices)) {
--sp;
}
stack[sp++] = points[j].index;
}
for (i = upper.length - 1; i >= 0; --i)
polygon.push(data[points[upper[i]][2]]); // add upper hull in r->l order
for (i = +skipLeft; i < lower.length - skipRight; ++i)
polygon.push(data[points[lower[i]][2]]); // add lower hull in l->r order
// construct the hull
var poly = [];
for (i = sp - 1; i >= 0; --i) poly.push(data[stack[i]]);
return poly;
return polygon;
}
hull.x = function(_) {
@ -111,11 +68,23 @@ d3.geom.hull = function(vertices) {
return hull;
};
// are three points in counter-clockwise order?
function d3_geom_hullCCW(i1, i2, i3, v) {
var t, a, b, c, d, e, f;
t = v[i1]; a = t[0]; b = t[1];
t = v[i2]; c = t[0]; d = t[1];
t = v[i3]; e = t[0]; f = t[1];
return (f - b) * (c - a) - (d - b) * (e - a) > 0;
// finds the 'upper convex hull' (see wiki link above)
// assumes points arg has >=3 elements, is sorted by x, unique in y
// returns array of indices into points in left to right order
function d3_geom_hullUpper(points) {
var n = points.length,
hull = [0, 1],
hs = 2; // hull size
for (var i = 2; i < n; i++) {
while (hs > 1 && !d3_isCCWTurn(points[hull[hs-2]], points[hull[hs-1]], points[i])) {
hs --;
}
hull[hs++] = i;
}
// we slice to make sure that the points we 'popped' from hull don't stay behind
return hull.slice(0, hs);
}
// comparator for ascending sort by x-coord first, y-coord second
function d3_geom_hullOrder(a, b) { return a[0] - b[0] || a[1] - b[1]; }

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

@ -10,6 +10,22 @@ function d3_sgn(x) {
return x > 0 ? 1 : x < 0 ? -1 : 0;
}
// returns true iff the [x,y] points a, b, c form a counter-clockwise turn in
// the traditional Cartesian coordinate system (i.e. x value grows from left
// to right, y value grows from bottom to top)
function d3_isCCWTurn(a, b, c) {
return d3_cross2d(a, b, c) > 0;
}
// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross
// product, in traditional Cartesian coordinate system (x value grows from
// left to right, y value grows from bottom to top). Returns a positive value
// if OAB makes a counter-clockwise turn, negative for clockwise turn, and
// zero if the points are collinear.
function d3_cross2d(o, a, b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
function d3_acos(x) {
return x > 1 ? 0 : x < -1 ? π : Math.acos(x);
}

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

@ -28,13 +28,37 @@ suite.addBatch({
assert.deepEqual(h([[200, 200], [760, 300]]), []);
},
"for three points": function(h) {
assert.deepEqual(h([[200, 200], [760, 300], [500, 500]]), [[500, 500], [760, 300], [200, 200]]);
assert.deepEqual(h([[200, 200], [760, 300], [500, 500]]), [[760, 300], [200, 200], [500, 500]]);
},
"for four points": function(h) {
assert.deepEqual(h([[200, 200], [760, 300], [500, 500], [400, 400]]), [[500, 500], [760, 300], [200, 200]]);
assert.deepEqual(h([[200, 200], [760, 300], [500, 500], [400, 400]]), [[760, 300], [200, 200], [500, 500]]);
},
"returns a counter-clockwise polygon": function(h) {
assert.greater(_.geom.polygon(h([[200, 200], [760, 300], [500, 500], [400, 400]])).area(), 0);
},
"handles points with duplicate ordinates": function(h) {
assert.deepEqual(h([[-10, -10], [10, 10], [10, -10], [-10, 10]]), [[10, 10], [10, -10], [-10, -10], [-10, 10]]);
},
"handles overlapping upper and lower hulls": function(h) {
assert.deepEqual(h([[0, -10], [0, 10], [0, 0], [10, 0], [-10, 0]]), [[10, 0], [0, -10], [-10, 0], [0, 10]]);
},
// Cases below taken from http://uva.onlinejudge.org/external/6/681.html
"for a set of 6 points with non-trivial hull": function(h) {
var poly = [[60,20], [250,140], [180,170], [79,140], [50,60], [60,20]];
var expectedHull = [[250,140], [60,20], [50,60], [79,140], [180,170]];
assert.deepEqual(h(poly), expectedHull);
},
"for a set of 12 points with non-trivial hull": function(h) {
var poly = [[50,60], [60,20], [70,45], [100,70], [125,90], [200,113], [250,140], [180,170], [105,140], [79,140], [60,85], [50,60]];
var expectedHull = [[250,140], [60,20], [50,60], [79,140], [180,170]];
assert.deepEqual(h(poly), expectedHull);
},
"for a set of 15 points with non-trivial hull": function(h) {
var poly = [[30,30], [50,60], [60,20], [70,45], [86,39], [112,60], [200,113], [250,50], [300,200], [130,240], [76,150], [47,76], [36,40], [33,35], [30,30]];
var expectedHull = [[300,200], [250,50], [60,20], [30,30], [47,76], [76,150], [130,240]];
assert.deepEqual(h(poly), expectedHull);
}
},
"the hull layout with custom accessors": {
@ -42,7 +66,7 @@ suite.addBatch({
return hull().x(function(d) { return d.x; }).y(function(d) { return d.y; });
},
"of four points": function(h) {
assert.deepEqual(h([{x: 200, y: 200}, {x: 760, y: 300}, {x: 500, y: 500}, {x: 400, y: 400}]), [{x: 500, y: 500}, {x: 760, y: 300}, {x: 200, y: 200}]);
assert.deepEqual(h([{x: 200, y: 200}, {x: 760, y: 300}, {x: 500, y: 500}, {x: 400, y: 400}]), [{x: 760, y: 300}, {x: 200, y: 200}, {x: 500, y: 500}]);
}
},
"the default hull layout applied directly": {
@ -56,10 +80,10 @@ suite.addBatch({
return h([[200, 200], [760, 300]]);
},
"for three points": function(h) {
assert.deepEqual(h([[200, 200], [760, 300], [500, 500]]), [[500, 500], [760, 300], [200, 200]]);
assert.deepEqual(h([[200, 200], [760, 300], [500, 500]]), [[760, 300], [200, 200], [500, 500]]);
},
"for four points": function(h) {
assert.deepEqual(h([[200, 200], [760, 300], [500, 500], [400, 400]]), [[500, 500], [760, 300], [200, 200]]);
assert.deepEqual(h([[200, 200], [760, 300], [500, 500], [400, 400]]), [[760, 300], [200, 200], [500, 500]]);
}
}
}