Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active January 16, 2020 13:22
Show Gist options
  • Save emeeks/ccc0368f6fb127d60b7c to your computer and use it in GitHub Desktop.
Save emeeks/ccc0368f6fb127d60b7c to your computer and use it in GitHub Desktop.
Circular Brush 2

A draft of d3.svg.circularBrush that visualizes the mouse position as a handle to emphasize the precision of control over the brush handle.

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 w/ Brush Handle</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: 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;
}
</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)")
.attr("class", "piebrush")
.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)
var _m = d3.mouse(d3.select("g.piebrush").node())
console.log(_m)
d3.selectAll(".brushhandle")
.attr("cx", _m[0])
.attr("cy", _m[1])
.attr("x2", _m[0])
.attr("y2", _m[1])
}
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() {
d3.select("g.piebrush")
.append("line")
.attr("class", "brushhandle")
.style("stroke", "brown")
.style("stroke-width", "2px")
d3.select("g.piebrush").append("circle")
.attr("class", "brushhandle")
.style("fill", "brown")
.attr("r", 5)
}
function pieBrushEnd() {
d3.selectAll(".brushhandle").remove();
}
}
</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