Hi everybody I am trying to rotate a rectangle while the user drag it with mouse. The rectangle follow a circular curve.
Below I attach my solution that is perfect, but the mouse is always on top left corner of the rectangle. I want that the mouse would be always in the center of the rectangle during dragging. How can I control it ?
Solution:
var drag = d3.drag().on("drag", function () {
var rect = d3.select(this);
var theta = Math.atan2(d3.event.y - height/2, d3.event.x - width/2) * 180 / Math.PI
rect
.attr("x", d3.event.x)
.attr("y", d3.event.y)
.attr('transform', `rotate(${theta + 90}, ${d3.event.x}, ${d3.event.y})`)
})
Full code of my solution you can see here: https://jsfiddle.net/hsspve49/
Offset the x and y attribute in the drag handler by the size of your rectangle, e.g.:
...
.attr("x", d3.event.x - 15) // half the width
.attr("y", d3.event.y - 35) // half the height
...
Related
In my recent project, I am interested in creating a clip-path which moves with my mousemove. My initial idea was simply to select and re-position the ellipsis with its attributes cx and cy using the mousemove coordinates, then selecting the rectangle and re-initializing its clip-path attribute.
This, however, does not seem to work. The only workable solution I have found so far is to delete the rectangle and the clip-path, then re-initializing them at the new coordinates. This works fine for the simple test-case below, but in my actual experiment, the object I'll try to clip is an externally loaded svg, and having to re-load it every mouseover tick might be prohibitively expensive.
Do you have any suggestions on how to achieve the same effect as I have shown below without re-initializing everything?
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/mathjs/lib/browser/math.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
</style>
</head>
<!-- Create a div where the graph will take place -->
<div id="my_datavisualization">
<svg id="click" xmlns="http://www.w3.org/2000/svg">
<defs>
<g id="pointer" transform="scale(0.5)">
<circle cx="0" cy="0" r="20" id="dragcircle" />
</g>
</defs>
</svg>
</div>
<body style='overflow:hidden'>
<script>
// Get the viewport height and width
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
// Fit to viewport
var height = vw*0.7;
var width = vw;
// Create the canvas. We will use only part of it for the main plot
var svg = d3.select("#click") // This selects the div
.attr("width", width) // This defines the canvas' width
.attr("height", height) // This defines the canvas' height
// define the clipPath
svg.append("clipPath") // define a clip path
.attr("id", "ellipse-clip") // give the clipPath an ID
.append("ellipse") // shape it as an ellipse
.attr("cx", 175) // position the x-centre
.attr("cy", 100) // position the y-centre
.attr("rx", 100) // set the x radius
.attr("ry", 50); // set the y radius
// draw clipped path on the screen
svg.append("rect") // attach a rectangle
.attr("id","cliprect")
.attr("x", 125) // position the left of the rectangle
.attr("y", 75) // position the top of the rectangle
.attr("clip-path", "url(#ellipse-clip)") // clip the rectangle
.style("fill", "lightgrey") // fill the clipped path with grey
.attr("height", 100) // set the height
.attr("width", 200); // set the width
// Shift the marker around on mouseover; restrict it to the contour
var movex
var movey
svg
.on("mousemove", function () {
// Get the current mouseover coordinates
movex = d3.event.x;
movey = d3.event.y;
// The only way I get this to work right now is by removing the previous clipped shape, then re-adding it
d3.select("#cliprect").remove()
d3.select("#ellipse-clip").remove()
// define the clipPath
svg.append("clipPath") // define a clip path
.attr("id", "ellipse-clip") // give the clipPath an ID
.append("ellipse") // shape it as an ellipse
.attr("cx", movex) // position the x-centre
.attr("cy", movey) // position the y-centre
.attr("rx", 100) // set the x radius
.attr("ry", 50); // set the y radius
// draw clipped path on the screen
svg.append("rect") // attach a rectangle
.attr("id","cliprect")
.attr("x", 125) // position the left of the rectangle
.attr("y", 75) // position the top of the rectangle
.attr("clip-path", "url(#ellipse-clip)") // clip the rectangle
.style("fill", "lightgrey") // fill the clipped path with grey
.attr("height", 100) // set the height
.attr("width", 200); // set the width
});
</script>
</body>
</html>
Use this in place of your mousemove callback
function() {
// Get the current mouseover coordinates
movex = d3.event.x;
movey = d3.event.y;
// move the clipPath
d3.select("#ellipse-clip") // selects the clipPath
.select("ellipse") // selects the ellipse
.attr("cx", movex) // position the x-centre
.attr("cy", movey) // position the y-centre
// move clipped path on the screen
svg.select("rect") // attach a rectangle
.attr("x", movex) // position the left of the rectangle
.attr("y", movey) // position the top of the rectangle
}
I want to rotate and zoom graphic around its center with D3.js. When I zoom graphic I want to zoom it with current aspect ratio and vice versa when I rotate graphic I want to zoom it to the current point that my mouse points. For zooming I use wheel of the mouse and for rotation I use the button of the mouse.
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
transform = d3.zoomIdentity;
var points = d3.range(2000).map(phyllotaxis(10));
var g = svg.append("g");
g.append("line")
.attr("x1", "20")
.attr("y1", "20")
.attr("x2", "60")
.attr("y2", "60")
.attr("stroke", "black")
.attr("stroke-width", "10");
svg.call(d3.drag()
.on("drag",onDrag)
)
// ##########################
var boxCenter = [100, 100];
// #############################
function onDrag(){
var x = d3.event.sourceEvent.pageX,
y = d3.event.sourceEvent.pageY;
var angle = Math.atan2(x - boxCenter[0],
- (y - boxCenter[1]) )*(180/Math.PI);
g.attr("transform", "rotate("+angle+")");
}
svg.call(d3.zoom()
.scaleExtent([1 / 2, 8])
.on("zoom", zoomed));
function zoomed() {
g.attr("transform", d3.event.transform);
}
function phyllotaxis(radius) {
var theta = Math.PI * (3 - Math.sqrt(5));
return function(i) {
var r = radius * Math.sqrt(i), a = theta * i;
return {
x: width / 2 + r * Math.cos(a),
y: height / 2 + r * Math.sin(a)
};
};
}
Here is my example:
https://jsfiddle.net/6Lyjz35L/
For the rotation around center to be correct at the initial zoom you need to add a 'transform-origin' attribute to 'g'.
g.attr("transform-origin", "50% 50%");
The other problems you're having stem from assigning the 'transform' attribute in two separate places. An element ('g') can only have one 'transform' attribute applied at a time, so you're overwriting one or the other each time you rotate or zoom. To fix this you can create a helper method which will append both of the transforms you want in a single string.
var currentAngle = 0;
var currentZoom = '';
function getTransform(p_angle, p_zoom) {
return `${p_zoom} rotate(${p_angle})`;
// return p_zoom + " rotate(" + p_angle + ")";
}
// In the rotate:
currentAngle = angle;
g.attr("transform", getTransform(currentAngle, currentZoom));
// In the zoom:
currentZoom = d3.event.transform;
g.attr("transform", getTransform(currentAngle, currentZoom));
There is one more issue which is introduced by the zoom, and that is that you'll have to calculate a new transform-origin at different zoom levels.
The issue I said was introduced by the zoom was actually the result of applying the operations in the incorrect order. Originally I applied the rotation and THEN then translation. It actually needs to be reversed, translation and THEN rotation. This will keep the correct transform-origin.
Here's a fiddle with those changes: https://jsfiddle.net/scmxcszz/1/
I am new on D3 v4 and D3 in general. I created a rectangle and I can drag it on the canvas. Now I want to dynamically rotate the rectangle basing on the circle's radius.
You can check my code here:
https://jsfiddle.net/n4m1r8nb/208/
I also tried to add rotate attribute on drag function, but if I add it, the rectangle move following the mouse as per x and y definition in the snippet you can see here below, without rotating, and an error appear ".rotate is not a function".
var drag = d3.drag().on("drag", function () {
d3.select(this)
.rotate(d3.event.x)
.attr("x", d3.event.x)
.attr("y", d3.event.y);
console.log("X: ", d3.event.x)
console.log("Y: ", d3.event.y)
})
You can see what I mean in this pic (http://imgur.com/a/APbu9). I wanna rotate the black rectangle as per the screenshot in that url.
Thanks in advance.
I have created a fiddle to demonstrate this: https://jsfiddle.net/hsspve49/
Relevant part of the code are in the drag handler:
var drag = d3.drag().on("drag", function () {
var rect = d3.select(this);
var theta = Math.atan2(d3.event.y - height / 2, d3.event.x - width / 2) * 180 / Math.PI
rect
.attr("x", d3.event.x)
.attr("y", d3.event.y)
.attr('transform', `rotate(${theta + 90}, ${d3.event.x}, ${d3.event.y})`)
})
I am trying to create a rectangle where I can resize and rotate it using handlers (small circles) located on the top of the rectangle. Similar to the most of the drawing tools that allow us to resize and rotate the shapes.
I added three circles on the top of my rectangle. One circle is for resizing the width of the rectangle (circle on the right side). Another rectangle is for resizing the height of the bar (circle on the top). Resizing the rectangle works perfectly.
margin = {top: 40, right: 20, bottom: 30, left: 70},
width = 600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var svg = d3.select('#canvas').attr("width",width).attr("height",height);
gContainer = svg.append('g')
.attr("class", "gContainer")
.attr("transform", function(d,i){
return "translate("+300+","+200+")"
})
gBars = gContainer.append('g').attr("class", "gBar");
gBars.append("rect")
.attr("class", "Bar")
.attr("fill","black")
.attr("width", 40)
.attr("height", function(d) { return height - 200})
.style("opacity", 0.5);
var handlerRadius = 3.5;
handlerPointsPosition=[];
elementWidth = Number(d3.select(".Bar").attr("width"));
elementHeight = Number(d3.select(".Bar").attr("height"));
x0 = 0 + (elementWidth/2) ;
y0 = 0 ;
x1 = 0 + (elementWidth);
y1 = 0 +(elementHeight/2) ;
x2= 0 + (elementWidth/2) ;
y2= -20;
handlerPointsPosition = [[x0,y0],[x1,y1],[x2,y2]];
var rectangleHandlers = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
gHandlerPoints= gContainer.selectAll("g.HandlerPoint")
.data(handlerPointsPosition)
.enter().append('g')
.attr("class", "gHandlerPoint")
.attr("id", function(d,i){return "gHandlerPoint_id_"+i;})
.attr("transform", function(d,i){
//console.log(d);
return "translate("+d[0]+","+d[1]+")"
})
.call(rectangleHandlers);
gHandlerPoints.append("circle")
.attr("class", "handlerPoint")
.style("fill", "white")
.style("stroke", "blue")
.attr("stroke","")
.attr("r",function(d,i){return (i == 2 ? 4: 3.5);})
.attr("id", function(d,i){return "HandlerPointId_"+i;})
gContainer.append("line")
.attr("class","handlerLine")
.attr("x1", (elementWidth/2) )
.attr("y1", 0- handlerRadius)
.attr("x2", (elementWidth/2) )
.attr("y2", -20 + handlerRadius)
.attr("stroke-width", 1)
.attr("stroke", "blue");
function updateHandlerPosition(id, dX, dY)
{
d3.select(id).attr("transform", function(d,i){
return "translate(" + [ dX, dY] + ")"
})
}
function dragstarted(d,i) {
dragIconX = d3.transform(d3.select(this).attr("transform")).translate[0];
dragIconY = d3.transform(d3.select(this).attr("transform")).translate[1];
barStartWidth = d3.select(".Bar").attr("width");
}
function dragged(d,i) {
barHeight = d3.select(".Bar").attr("height");
if(i == 0) // circle on the top edge of the bar
{
dragIconY = dragIconY + d3.mouse(this)[1];
updateHandlerPosition("#gHandlerPoint_id_"+i, dragIconX, dragIconY );
updateHandlerPosition("#gHandlerPoint_id_1", (barStartWidth), (barHeight/2) );
var x = d3.transform(d3.select(".gContainer").attr("transform")).translate[0];
var y = d3.transform(d3.select(".gContainer").attr("transform")).translate[1];
d3.select(".gContainer").attr("transform", function(d,i){
y = y + dragIconY;
return "translate(" + [ x , y] + ")"
})
console.log(height, barHeight, barHeight - Number(dragIconY));
d3.select(".Bar").attr("height", barHeight - Number(dragIconY));
}
else if (i==1) // circle on the right side of the bar
{
oldMouseX = dragIconX;
dragIconX = d3.mouse(this)[0]+dragIconX;
barWidth = dragIconX;
updateHandlerPosition("#gHandlerPoint_id_"+i, dragIconX, dragIconY );
updateHandlerPosition("#gHandlerPoint_id_0", (barWidth/2), 0 );
updateHandlerPosition("#gHandlerPoint_id_2", (barWidth/2), -20);
d3.select(".handlerLine").attr("x1",(barWidth/2)).attr("x2", (barWidth/2));
d3.select(".Bar").attr("width", Math.abs(dragIconX));
}
else if(i==3) //circle on very top
{
// code for rotation should come here.
}
}
Link to jsFiddle: http://jsfiddle.net/Q5Jag/2103/
I put the third circle for rotation (the circle on the very top). However, I have no idea how to fix the rotation. I want the rectangle to rotate when I drag the circle on the very top. I also want to be able to resize the circle accordingly when it is rotated.
Any idea?
You can calculate the rotation with respect to the center of your rectangle. Consider v1 as vector from center of rectangle to the rotation handle before rotation and v2 as a vector from center of rectangle to the rotation handle after rotation. Read here to know the math behind it.
// old vector
v1 = {};
v1.x = oldMouseX-gCenterX;
v1.y = oldMouseY-gCenterY;
// new vector
v2 = {};
v2.x = dragHandleX-gCenterX;
v2.y = dragHandleY-gCenterY;
angle = Math.atan2(v2.y,v2.x) - Math.atan2(v1.y,v1.x);
where
gCenterX and gCenterY are coordinates of the center of rectangle
oldMouseX and oldMouseY are coordinates of mouse pointer prior to rotation
dragHandleX and dragHandleY are coordinates of mouse pointer after rotation
Here is the complete code:
http://jsfiddle.net/Q5Jag/2109/
(But it fails if you mix rotation and resizing)
I'm trying to use D3.js to draw rectangles around a circle (think of drawing chairs around a table).
I've tried drawing each chair individually by setting it's x and y position and then rotating it, but I haven't had much luck.
I thought this might be a good approach:
group = container.append("g")
table = group.append("circle").
attr("cx", 100).
attr("cy", 100).
attr("r", 60).
attr("fill", "#FFF").
attr("stroke", "#b8b8b8").
attr("stroke-width", "2")
group
.append("rect")
.attr("x", 90)
.attr("y", 10)
.attr("width", 20)
.attr("height", 20)
group.attr("transform", "rotate(30, 100, 100)")
But I can't figure out how to make a transformation, redraw, and then make another transformation. Any ideas?
I ended up building upon my idea of using a rotation. Then, I just had to brush up a little on my geometry.
First, I find a point on the circle. The equation for finding a point on the circle, given you know the center-x and center-y (cx and cy) of the circle, is:
x = cx + (r * sin(a))
y = cx + (r * cos(a))
Where r is radius and a is the angle on the circle in radians.
Then, I drew every rectangle at the point (0, 0) and rotated them around the center of the circle.
Here is what my solution ended up looking like:
radians = (Math.PI * 0) / 180.0
x = 100 + (70 * Math.sin(radians))
y = 100 + (70 * Math.cos(radians))
for i in [0..360] by 30
container
.append("rect")
.attr("x", x - 10)
.attr("y", y)
.attr("width", 20)
.attr("height", 20)
.attr("transform", "rotate(#{i}, 100, 100)")
Note: Subtracting 10 from x accounts for the width of the rectangle.