Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active March 17, 2016 02:16
Show Gist options
  • Save emeeks/905c4691f343fc4780bd to your computer and use it in GitHub Desktop.
Save emeeks/905c4691f343fc4780bd to your computer and use it in GitHub Desktop.
Circular Brush 1

A first draft of d3.svg.circularBrush which allows you to select cyclical ranges. It's patterned after d3.svg.brush, so you drag the resize handles to change the extent or drag the extent to move it around the selection surface. In this case it's mapped to an array of 24 values (1-24), each represented in a pie arc, thus implying a clock. By dragging and resizing the brush, you select the corresponding arcs.

Cyclical time is an obvious application for a brush like this, allowing you to select a period of time such as "between 10PM and 2AM" to filter your results by.

d3.svg.circularbrush = function() {
var _extent = [0,Math.PI * 2];
var _circularbrushDispatch = d3.dispatch('brushstart', 'brushend', 'brush');
var _arc = d3.svg.arc().innerRadius(50).outerRadius(100);
var _brushData = [
{startAngle: _extent[0], endAngle: _extent[1], class: "extent"},
{startAngle: _extent[0] - .2, endAngle: _extent[0], class: "resize e"},
{startAngle: _extent[1], endAngle: _extent[1] + .2, class: "resize w"}
];
var _newBrushData = [];
var d3_window = d3.select(window);
var _origin;
var _brushG;
var _handleSize = .2;
var _scale = d3.scale.linear().domain(_extent).range(_extent);
function _circularbrush(_container) {
_brushG = _container
.append("g")
.attr("class", "circularbrush");
_brushG
.selectAll("path.circularbrush")
.data(_brushData)
.enter()
.insert("path", "path.resize")
.attr("d", _arc)
.attr("class", function(d) {return d.class + " circularbrush"})
_brushG.select("path.extent")
.on("mousedown.brush", resizeDown)
_brushG.selectAll("path.resize")
.on("mousedown.brush", resizeDown)
return _circularbrush;
}
_circularbrush.extent = function(_value) {
var _d = _scale.domain();
var _r = _scale.range();
var _actualScale = d3.scale.linear()
.domain([-_d[1],_d[0],_d[0],_d[1]])
.range([_r[0],_r[1],_r[0],_r[1]])
if (!arguments.length) return [_actualScale(_extent[0]),_actualScale(_extent[1])];
_extent = [_scale.invert(_value[0]),_scale.invert(_value[1])];
return this
}
_circularbrush.handleSize = function(_value) {
if (!arguments.length) return _handleSize;
_handleSize = _value;
return this
}
_circularbrush.innerRadius = function(_value) {
if (!arguments.length) return _arc.innerRadius();
_arc.innerRadius(_value);
return this
}
_circularbrush.outerRadius = function(_value) {
if (!arguments.length) return _arc.outerRadius();
_arc.outerRadius(_value);
return this
}
_circularbrush.range = function(_value) {
if (!arguments.length) return _scale.range();
_scale.range(_value);
return this
}
d3.rebind(_circularbrush, _circularbrushDispatch, "on");
return _circularbrush;
function resizeDown(d) {
var _mouse = d3.mouse(_brushG.node());
_originalBrushData = {startAngle: _brushData[0].startAngle, endAngle: _brushData[0].endAngle};
_origin = _mouse;
if (d.class == "resize e") {
d3_window
.on("mousemove.brush", function() {resizeMove("e")})
.on("mouseup.brush", extentUp);
}
else if (d.class == "resize w") {
d3_window
.on("mousemove.brush", function() {resizeMove("w")})
.on("mouseup.brush", extentUp);
}
else {
d3_window
.on("mousemove.brush", function() {resizeMove("extent")})
.on("mouseup.brush", extentUp);
}
_circularbrushDispatch.brushstart();
}
function resizeMove(_resize) {
var _mouse = d3.mouse(_brushG.node());
var _current = Math.atan2(_mouse[1],_mouse[0]);
var _start = Math.atan2(_origin[1],_origin[0]);
if (_resize == "e") {
var clampedAngle = Math.max(Math.min(_originalBrushData.startAngle + (_current - _start), _originalBrushData.endAngle), _originalBrushData.endAngle - (2 * Math.PI));
if (_originalBrushData.startAngle + (_current - _start) > _originalBrushData.endAngle) {
clampedAngle = _originalBrushData.startAngle + (_current - _start) - (Math.PI * 2);
}
else if (_originalBrushData.startAngle + (_current - _start) < _originalBrushData.endAngle - (Math.PI * 2)) {
clampedAngle = _originalBrushData.startAngle + (_current - _start) + (Math.PI * 2);
}
var _newStartAngle = clampedAngle;
var _newEndAngle = _originalBrushData.endAngle;
}
else if (_resize == "w") {
var clampedAngle = Math.min(Math.max(_originalBrushData.endAngle + (_current - _start), _originalBrushData.startAngle), _originalBrushData.startAngle + (2 * Math.PI))
if (_originalBrushData.endAngle + (_current - _start) < _originalBrushData.startAngle) {
clampedAngle = _originalBrushData.endAngle + (_current - _start) + (Math.PI * 2);
}
else if (_originalBrushData.endAngle + (_current - _start) > _originalBrushData.startAngle + (Math.PI * 2)) {
clampedAngle = _originalBrushData.endAngle + (_current - _start) - (Math.PI * 2);
}
var _newStartAngle = _originalBrushData.startAngle;
var _newEndAngle = clampedAngle;
}
else {
var _newStartAngle = _originalBrushData.startAngle + (_current - _start * 1);
var _newEndAngle = _originalBrushData.endAngle + (_current - _start * 1);
}
_newBrushData = [
{startAngle: _newStartAngle, endAngle: _newEndAngle, class: "extent"},
{startAngle: _newStartAngle - _handleSize, endAngle: _newStartAngle, class: "resize e"},
{startAngle: _newEndAngle, endAngle: _newEndAngle + _handleSize, class: "resize w"}
]
_brushG
.selectAll("path.circularbrush")
.data(_newBrushData)
.attr("d", _arc)
if (_newStartAngle > (Math.PI * 2)) {
_newStartAngle = (_newStartAngle - (Math.PI * 2));
}
else if (_newStartAngle < -(Math.PI * 2)) {
_newStartAngle = (_newStartAngle + (Math.PI * 2));
}
if (_newEndAngle > (Math.PI * 2)) {
_newEndAngle = (_newEndAngle - (Math.PI * 2));
}
else if (_newEndAngle < -(Math.PI * 2)) {
_newEndAngle = (_newEndAngle + (Math.PI * 2));
}
_extent = ([_newStartAngle,_newEndAngle]);
_circularbrushDispatch.brush();
}
function extentUp() {
_brushData = _newBrushData;
d3_window.on("mousemove.brush", null).on("mouseup.brush", null);
_circularbrushDispatch.brushend();
}
}
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Circular Brush</title>
<meta charset="utf-8" />
</head>
<style>
#viz, svg {
width: 1000px;
height: 1000px;
}
.resize {
fill-opacity: .5;
cursor: move;
stroke: black;
stroke-width: 1px;
}
.extent {
fill-opacity: .25;
fill: rgb(205,130,42);
cursor: grab;
stroke: black;
stroke-width: 1px;
}
.e {
fill: rgb(111,111,111);
cursor: move;
}
.w {
fill: rgb(169,169,169);
cursor: move;
}
path.piehours {
fill: rgb(246,139,51);
stroke: black;
stroke-width: 1px;
}
</style>
<script>
function makeViz() {
piebrush = d3.svg.circularbrush();
piebrush
.range([1,24])
.innerRadius(80)
.outerRadius(120)
.on("brushstart", pieBrushStart)
.on("brushend", pieBrushEnd)
.on("brush", pieBrush);
d3.select("svg")
.append("g")
.attr("transform", "translate(250,250)")
.call(piebrush);
var hours = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24];
var pie = d3.layout.pie().value(function() {return 1}).sort(d3.ascending);
var pieArc = d3.svg.arc().innerRadius(130).outerRadius(160);
d3.select("svg")
.append("g")
.attr("transform", "translate(250,250)")
.selectAll("path")
.data(pie(hours))
.enter()
.append("path")
.attr("class", "piehours")
.attr("d", pieArc)
.on("click", function(d) {console.log(d)})
function pieBrush() {
d3.selectAll("path.piehours")
.style("fill", piebrushIntersect)
}
function piebrushIntersect(d,i) {
var _e = piebrush.extent();
if (_e[0] < _e[1]) {
var intersect = (d.data >= _e[0] && d.data <= _e[1]);
}
else {
var intersect = (d.data >= _e[0]) || (d.data <= _e[1]);
}
return intersect ? "rgb(241,90,64)" : "rgb(231,231,231)"
}
function pieBrushStart() {
var _e = piebrush.extent();
}
function pieBrushEnd() {
var _e = piebrush.extent();
}
}
</script>
<body onload="makeViz()">
<div id="viz"><svg></svg><div id="buttons"></div></div>
<footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="d3.svg.circularbrush.js" charset="utf-8" type="text/javascript"></script>
</footer>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment