Skip to content

Instantly share code, notes, and snippets.

@GerHobbelt
Forked from gtb104/README.markdown
Created September 16, 2012 14:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save GerHobbelt/3732612 to your computer and use it in GitHub Desktop.
Save GerHobbelt/3732612 to your computer and use it in GitHub Desktop.
Polybrush Example

Here's a d3 plugin that allows you to create a polygon selection. You instantiate it just like d3.svg.brush.

var brush = d3.svg.polybrush();

It has an extra public method that 'brush' does not, and that's 'isWithinExtent(x, y)'. You can use this method to test if a given point falls within the drawn extent.

if (brush.isWithinExtent(x, y)) {
  console.log("I'm inside!");
}

Usage:
Click to add an anchor point, double click to close the selection.
Drag the selection to reposition it.
Double click outside the selection to clear it.

Areas For Improvement:

  1. Add anchor handles to allow the repositioning of an anchor point.
  2. Better viewport edge detection. Right now it simply stops moving if you try to move the selection out of the viewport.
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js"></script>
<script type="text/javascript" src="polybrush.js"></script>
<style type="text/css">
.brush .extent {
stroke: #000;
stroke-width: 1.5px;
fill: #000;
fill-opacity: 0.3;
}
.voronoi {
fill: steelblue;
fill-opacity: 0.4;
stroke: steelblue;
stroke-opacity: 0.5;
}
.voronoi.selected {
fill: red;
fill-opacity: 0.4;
stroke: darkred;
stroke-opacity: 0.5;
}
.point.selected {
fill: red;
stroke: darkred;
stroke-width: 1;
}
</style>
</head>
<body>
<div id="chart"></div>
<script type="text/javascript">
var w = 960,
h = 500;
var brush = d3.svg.polybrush()
.x(d3.scale.linear().range([0, w]))
.y(d3.scale.linear().range([0, h]))
.on("brushstart", function() {
svg.selectAll(".selected").classed("selected", false);
})
.on("brush", function() {
// set the 'selected' class for the circle
svg.selectAll(".point").classed("selected", function(d) {
//get the associated circle
var id = d3.select(this).attr("id");
var i = id.substr(id.indexOf("-")+1, id.length);
var vornoi = d3.select("#path-"+i);
// set the 'selected' class for the path
if (brush.isWithinExtent(d[0], d[1])) {
vornoi.classed("selected", true);
return true;
} else {
vornoi.classed("selected", false);
return false;
}
});
});
var vertices = d3.range(200).map(function(d) {
return [Math.random() * w, Math.random() * h];
});
var svg = d3.select("#chart")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
var paths, points, clips;
clips = svg.append("svg:g").attr("id", "point-clips");
points = svg.append("svg:g").attr("id", "points");
paths = svg.append("svg:g").attr("id", "point-paths");
clips.selectAll("clipPath")
.data(vertices)
.enter().append("svg:clipPath")
.attr("id", function(d, i) { return "clip-"+i;})
.append("svg:circle")
.attr('cx', function(d) { return d[0]; })
.attr('cy', function(d) { return d[1]; })
.attr('r', 20);
paths.selectAll("path")
.data(d3.geom.voronoi(vertices))
.enter().append("svg:path")
.attr("d", function(d) { return "M" + d.join(",") + "Z"; })
.attr("id", function(d,i) { return "path-"+i; })
.attr("clip-path", function(d,i) { return "url(#clip-"+i+")"; })
.attr("class", "voronoi");
points.selectAll("circle")
.data(vertices)
.enter().append("svg:circle")
.attr("class", "point")
.attr("id", function(d, i) { return "point-"+i; })
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 2)
.attr('stroke', 'none');
svg.append("svg:g")
.attr("class", "brush")
.call(brush);
</script>
</body>
</html>
(function(d3) {
d3.svg.polybrush = function() {
var dispatch = d3.dispatch("brushstart", "brush", "brushend"),
x = null,
y = null,
extent = [],
firstClick = true,
firstTime = true,
wasDragged = false,
origin = null,
line = d3.svg.line()
.x(function(d) {
return d[0];
})
.y(function(d) {
return d[1];
});
var brush = function(g) {
g.each(function() {
var bg, e, fg;
g = d3.select(this);
bg = g.selectAll(".background").data([0]);
fg = g.selectAll(".extent").data([extent]);
g.style("pointer-events", "all").on("click.brush", addAnchor);
bg.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("cursor", "crosshair");
fg.enter().append("path").attr("class", "extent").style("cursor", "move");
if (x) {
e = scaleExtent(x.range());
bg.attr("x", e[0]).attr("width", e[1] - e[0]);
}
if (y) {
e = scaleExtent(y.range());
bg.attr("y", e[0]).attr("height", e[1] - e[0]);
}
});
};
var drawPath = function() {
return d3.selectAll("g path").attr("d", function(d) {
return line(d) + "Z";
});
};
var scaleExtent = function(domain) {
var start, stop;
start = domain[0];
stop = domain[domain.length - 1];
if (start < stop) {
return [start, stop];
} else {
return [stop, start];
}
};
var withinBounds = function(point) {
var rangeX, rangeY, _x, _y;
rangeX = scaleExtent(x.range());
rangeY = scaleExtent(y.range());
_x = Math.max(rangeX[0], Math.min(rangeX[1], point[0]));
_y = Math.max(rangeY[0], Math.min(rangeY[1], point[1]));
return point[0] === _x && point[1] === _y;
};
var moveAnchor = function(target) {
var moved, point;
point = d3.mouse(target);
if (firstTime) {
extent.push(point);
firstTime = false;
} else {
if (withinBounds(point)) {
extent.splice(extent.length - 1, 1, point);
}
drawPath();
dispatch.brush();
}
};
var closePath = function() {
var w;
w = d3.select(window);
w.on("dblclick.brush", null).on("mousemove.brush", null);
firstClick = true;
if (extent.length === 2 && extent[0][0] === extent[1][0] && extent[0][1] === extent[1][1]) {
extent.splice(0, extent.length);
}
d3.select(".extent").on("mousedown.brush", moveExtent);
return dispatch.brushend();
};
var addAnchor = function() {
var g, w,
_this = this;
g = d3.select(this);
w = d3.select(window);
firstTime = true;
if (wasDragged) {
wasDragged = false;
return;
}
if (firstClick) {
extent.splice(0, extent.length);
firstClick = false;
d3.select(".extent").on("mousedown.brush", null);
w.on("mousemove.brush", function() {
return moveAnchor(_this);
}).on("dblclick.brush", closePath);
dispatch.brushstart();
}
if (extent.length > 1) {
extent.pop();
}
extent.push(d3.mouse(this));
return drawPath();
};
var dragExtent = function(target) {
var checkBounds, fail, p, point, scaleX, scaleY, updateExtentPoint, _i, _j, _len, _len1;
point = d3.mouse(target);
scaleX = point[0] - origin[0];
scaleY = point[1] - origin[1];
fail = false;
origin = point;
updateExtentPoint = function(p) {
p[0] += scaleX;
p[1] += scaleY;
};
for (_i = 0, _len = extent.length; _i < _len; _i++) {
p = extent[_i];
updateExtentPoint(p);
}
checkBounds = function(p) {
if (!withinBounds(p)) {
fail = true;
}
return fail;
};
for (_j = 0, _len1 = extent.length; _j < _len1; _j++) {
p = extent[_j];
checkBounds(p);
}
if (fail) {
return;
}
drawPath();
return dispatch.brush({
mode: "move"
});
};
var dragStop = function() {
var w;
w = d3.select(window);
w.on("mousemove.brush", null).on("mouseup.brush", null);
wasDragged = true;
return dispatch.brushend();
};
var moveExtent = function() {
var _this = this;
d3.event.stopPropagation();
d3.event.preventDefault();
if (firstClick && !brush.empty()) {
d3.select(window).on("mousemove.brush", function() {
return dragExtent(_this);
}).on("mouseup.brush", dragStop);
origin = d3.mouse(this);
}
};
brush.isWithinExtent = function(x, y) {
var i, j, len, p1, p2, ret, _i, _len;
len = extent.length;
j = len - 1;
ret = false;
for (i = _i = 0, _len = extent.length; _i < _len; i = ++_i) {
p1 = extent[i];
p2 = extent[j];
if ((p1[1] > y) !== (p2[1] > y) && x < (p2[0] - p1[0]) * (y - p1[1]) / (p2[1] - p1[1]) + p1[0]) {
ret = !ret;
}
j = i;
}
return ret;
};
brush.x = function(z) {
if (!arguments.length) {
return x;
}
x = z;
return brush;
};
brush.y = function(z) {
if (!arguments.length) {
return y;
}
y = z;
return brush;
};
brush.extent = function(z) {
if (!arguments.length) {
return extent;
}
extent = z;
return brush;
};
brush.clear = function() {
extent.splice(0, extent.length);
return brush;
};
brush.empty = function() {
return extent.length === 0;
};
d3.rebind(brush, dispatch, "on");
return brush;
};
})(d3);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment