block by emeeks ccc0368f6fb127d60b7c

Circular Brush 2

Full Screen

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

index.html

<html xmlns="//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>

d3.svg.circularbrush.js

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();
	}


}