Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active March 29, 2016 19:39
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emeeks/5850fa6583bfd90e7899 to your computer and use it in GitHub Desktop.
Save emeeks/5850fa6583bfd90e7899 to your computer and use it in GitHub Desktop.
Circular Brush 3

An example of d3.svg.circularBrush that allows you to select a range of months, days of the week and/or hours.

A circular brush may seem like a novelty, but the reality is that we deal with cyclical data all the time, and traditional linear selectors are ill-suited for that kind of data. You can't select winter on a linear brush with January on the left and December on the right, nor could you select commonplace things like "nighttime" or "three day weekend" on default linear brushes.

There are other cyclical datatypes besides time, but they make for good examples. I guess the Mayans were right.

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 Selecting Months, Days, Hours</title>
<meta charset="utf-8" />
</head>
<style>
#viz, svg {
width: 1000px;
height: 1000px;
}
#info {
position: absolute;
left:20px;
top:0px;
}
.resize {
fill-opacity: .5;
cursor: move;
stroke: black;
stroke-width: 1px;
}
.extent {
fill-opacity: .25;
fill: rgb(205,130,42);
cursor: hand;
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;
}
g.hoursbrush .extent {
fill: rgb(225,50,42);
}
g.monthsbrush .extent {
fill: rgb(185,200,42);
}
p {
font-weight: 900;
font-family: helvetica, sans;
pointer-events: none;
}
</style>
<script>
function makeViz() {
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
hours = function(rawHours) {
var _suffix = "AM";
if (rawHours > 12) {
rawHours = rawHours - 12;
_suffix = "PM"
}
return rawHours + _suffix
}
hoursbrush = d3.svg.circularbrush()
.range([1,24])
.innerRadius(20)
.outerRadius(60)
.on("brush", pieBrush);
daysbrush = d3.svg.circularbrush()
.range([0,6])
.innerRadius(80)
.outerRadius(120)
.on("brush", pieBrush);
monthsbrush = d3.svg.circularbrush()
.range([0,11])
.innerRadius(140)
.outerRadius(180)
.on("brush", pieBrush);
d3.select("svg")
.append("g")
.attr("class", "hoursbrush")
.attr("transform", "translate(250,250)")
.call(hoursbrush);
d3.select("svg")
.append("g")
.attr("class", "daysbrush")
.attr("transform", "translate(250,250)")
.call(daysbrush);
d3.select("svg")
.append("g")
.attr("class", "monthsbrush")
.attr("transform", "translate(250,250)")
.call(monthsbrush);
function pieBrush() {
var _h = hoursbrush.extent();
var _d = daysbrush.extent();
var _m = monthsbrush.extent();
d3.select("#hours")
.html("Between " + hours(Math.round(_h[0])) + " and " + hours(Math.round(_h[1])) + "")
d3.select("#days")
.html("" + days[Math.round(_d[0])] + " through " + days[Math.round(_d[1])] + "")
d3.select("#months")
.html("From " + months[Math.round(_m[0])] + " till " + months[Math.round(_m[1])] + "")
}
}
</script>
<body onload="makeViz()">
<div id="viz"><svg></svg><div id="info"><p id="hours">Between 1AM and Midnight</p><p id="days">Between Monday and Sunday</p><p id="months"></p></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>
@timelyportfolio
Copy link

very creative use and implementation; nicely done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment