Skip to content

Instantly share code, notes, and snippets.

@tomgp
Last active April 19, 2023 21:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomgp/f39ccb9d4c17ced4e3d2 to your computer and use it in GitHub Desktop.
Save tomgp/f39ccb9d4c17ced4e3d2 to your computer and use it in GitHub Desktop.
drag circles around in a ring

Sometimes you might want to constrain a dragged item to a circular path...

<html>
<head>
<title>ring control</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
.handle{
fill:#FFF;
stroke:#000;
stroke-width:4;
cursor: all-scroll;
}
.handle.active{
stroke:#F00;
}
.ring{
fill:none;
stroke:#000;
stroke-width:4;
}
</style>
</head>
<body>
<div class="ring-input"></div>
</body>
<script type="text/javascript">
var values = [
{value:33, label:'A' },
{value:33, label:'B' },
{value:33, label:'C' },
{value:33, label:'D' },
{value:33, label:'E' }
];
var total = 0;
var dragging = null;
values.forEach(function(d){
d.absoluteValue = total;
total += d.value;
});
console.log(values);
var height = 300, width = 300, margin = {top:20,left:20,bottom:20,right:20};
var radius = (height - margin.top - margin.bottom)/2;
var parent = d3.select('.ring-input').append('svg')
.attr({
height:height,
width:width
}).append('g').attr('transform','translate('+margin.left+','+margin.top+')')
parent.append('line').attr('id','test-line')
var angularScale = d3.scale.linear().range([0,360]).domain([0,total]);
var ring = parent.append('g').attr('id','rim').attr('transform','translate('+radius+','+radius+')');
ring.append('circle').attr({
r:radius,
'class':'ring'
})
var handles = parent.append('g').attr('id','handles').attr('transform','translate('+radius+','+radius+')');
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("drag", dragmove)
.on('dragend', function(){ d3.select(this).classed('active',false); });
//position the handles based on the input values
function drawHandles(){
var join = handles.selectAll('circle').data(values);
join.enter()
.append('circle').attr({
r:10,
'class':'handle'
}).on("mouseover", function(){
d3.select(this).classed('active',true);
})
.on("mouseout", function(){
d3.select(this).classed('active',false);
})
.call(drag);
join.attr({
transform:function(d){
return 'rotate(' + angularScale(d.absoluteValue) + ') translate(' +radius + ',0)'
}
})
}
drawHandles();
function dragmove(d,i) {
d3.select(this).classed('active',true);
var coordinates = d3.mouse(parent.node());
var x = coordinates[0]-radius;
var y = coordinates[1]-radius;
var newAngle = Math.atan2( y , x )* 57.2957795;
if(newAngle<0){
newAngle = 360 + newAngle;
}
d.absoluteValue = angularScale.invert(newAngle);
//REDRAW HANDLES
drawHandles()
}
</script>
</html>
@sander-spruit
Copy link

sander-spruit commented Apr 19, 2023

Very useful but now outdated. I tried updating to the latest version (5), but cannot seem to get it to work.

Updated to the following;

<html>
<head>
	<title>ring control</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.4/d3.min.js" charset="utf-8"></script>	<style type="text/css">
	.handle{
		fill:#FFF;
		stroke:#000;
		stroke-width:4;
		cursor: all-scroll;
	}

	.handle.active{
		stroke:#F00;
	}

	.ring{
		fill:none;
		stroke:#000;
		stroke-width:4;
	}
	</style>
</head>
<body>
<div class="ring-input"></div>
</body>

<script type="text/javascript">
var values  = [
	{value:33, label:'A' },
	{value:33, label:'B' },
	{value:33, label:'C' },
	{value:33, label:'D' },
	{value:33, label:'E' }
];
var total = 0;
var dragging = null;

values.forEach(function(d){
 	d.absoluteValue = total;
 	total += d.value;
});

console.log(values);
var height = 300, width = 300, margin = {top:20,left:20,bottom:20,right:20};
var radius = (height - margin.top - margin.bottom)/2;

var parent = d3.select(".ring-input").append("svg").attr("width", 500).attr("height", 500).append('g').attr('transform','translate('+margin.left+','+margin.top+')')

parent.append('line').attr('id','test-line')

var angularScale = d3.scaleLinear().range([0,360]).domain([0,total]);

var ring = parent.append('g').attr('id','rim').attr('transform','translate('+radius+','+radius+')');
ring.append('circle').attr({
	r:radius,
	'class':'ring'
})

var handles = parent.append('g').attr('id','handles').attr('transform','translate('+radius+','+radius+')');
var drag = d3.drag()
    .subject(function(d) { return d; })
    .on("drag", dragmove)
    .on('end', function(){ d3.select(this).classed('active',false); });

//position the handles based on the input values
function drawHandles(){
	var join = handles.selectAll('circle').data(values);
	join.enter()
		.append('circle').attr({
			r:10,
			'class':'handle'
		})
        .on("mouseover", function(){
    		d3.select(this).classed('active',true);
    	})
    	.on("mouseout", function(){
    		d3.select(this).classed('active',false);
    	})
		.call(drag);

	join.attr({
		transform:function(d){
			return 'rotate(' + angularScale(d.absoluteValue) + ') translate(' +radius + ',0)'
		}
	})
}

drawHandles();

function dragmove(d,i) {
	d3.select(this).classed('active',true);
	var coordinates = d3.mouse(parent.node());
 	var x = coordinates[0]-radius;
 	var y = coordinates[1]-radius;
  	var newAngle = Math.atan2( y , x )* 57.2957795;
  	if(newAngle<0){
  		newAngle = 360 + newAngle;
  	}
 	d.absoluteValue = angularScale.invert(newAngle);
  	//REDRAW HANDLES
  	drawHandles()
}

</script>
</html>

But that shows

Uncaught TypeError: Cannot read properties of null (reading 'on') at drawHandles (73:9)

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