A draft of d3.svg.circularBrush that visualizes the mouse position as a handle to emphasize the precision of control over the brush handle.
Last active
January 16, 2020 13:22
-
-
Save emeeks/ccc0368f6fb127d60b7c to your computer and use it in GitHub Desktop.
Circular Brush 2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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