d3 SVG drag drop plus rotate with button - javascript

I'm attempting to create a program where an image can be dragged and dropped but also rotated at the push of a button(after being dragged, and rotated on its own axis). I can successfully make an image drag and drop but cannot get the rotate to work correctly. I think the solution would be to use a "transform","translate" for positioning the image at the start; and for the dragging instead of "d3.event" however I am unsure of how to do this. I'm using SVG with d3 library. I wish to keep my: dragstarted, dragged and dragended separate as will be adding further code later.
var svgWidth = parseInt(document.getElementById("canvas").getAttribute("width")),
selected = null;
canvas = d3.select('#canvas');
canvas.append("image")
.attr('width', 17)
.attr('height', 59)
.attr("x", svgWidth - 40)
.attr("y", 100)
.attr("xlink:href", "https://lorempixel.com/100/100/cats")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
canvas.append("image")
.attr('width', 80)
.attr('height', 38)
.attr("x", svgWidth - 90)
.attr("y", 200)
.attr("xlink:href", "https://lorempixel.com/100/100/sports")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(d){
d3.select(this).raise().classed("active", true);
}
function dragged(d){
d3.select(this)
.attr("x", d3.event.x - parseInt(d3.select('image').attr("width")) / 2)
.attr("y", d3.event.y - parseInt(d3.select('image').attr("height")) / 2);
}
function dragended(d){
d3.select(this).classed("active", false);
selected = this;
}
window.onload=function(){
document.getElementById("rotate").addEventListener("click", function(){
if(selected != null){
var x = selected.getAttribute("x"),
y = selected.getAttribute("y");
selected.setAttribute("transform","translate(" + x / 2 + "," + y / 2 +")" + "rotate(90)");
}
});
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>test</title>
<link rel="stylesheet" href="styles/style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js">
</script>
</head>
<body>
<svg id="canvas" width="960px" height="500px"></svg>
<button id="rotate">Rotate</button>
</body>
</html>

Nevermind I fixed it. Didn't realise the x and y values didn't have to be set on the images to use d3.event.x and d3.event.y . Meaning I could use those values on the transform when the image was being dragged.

Related

Problem getting datum to pass correctly on drag in d3 v7

Weird issue I'm seeing using drag in d3 v7 (also seeing this with v6). If I define .call(drag) such that
var drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged )
.on("end", dragended);
and
function dragged(event,d) {
{{things that depend on event and d}}
}
then everything works as expected.
However, if I try,
var drag = d3.drag()
.on("start", dragstarted)
.on("drag", (event,d) => { dragged(event,d) } )
.on("end", dragended);
I get the log yelling at me that Uncaught TypeError: d is undefined. What's going on here?
Happy to provide a minimal example tomorrow. Just hoping someone may have seen this before..? I'm fairly new to d3 (and to JS for that matter, ha) and I hope I've got the revised post-v6 syntax right.
Edit: In my effort to create a minimal example, I realized that the TypeError may have been obfuscating the real issue, which is that this isn't being passed correctly. So strap in.
First, a minimal example that replicates the issue. Modified from this example. Not my situation, but same principle. Put this in the body tag:
<body>
<svg></svg>
<script>
function dragstarted(event, d) {
d3.select(this).raise().attr("stroke", "black");
}
function dragged(event, d) {
d.x = event.x;
d.y = event.y;
d3.select(this).attr("cx", (d) => d.x).attr("cy", (d) => d.y);
}
function dragended(event, d) {
d3.select(this).attr("stroke", null);
}
var drag = d3.drag()
.on("start", dragstarted)
.on("drag", function(event,d) {dragged(event,d)} )
.on("end", dragended);
var width = 400;
var height = 400;
var radius = 20;
const svg = d3.select("svg")
.attr("viewBox", [0, 0, width, height])
.attr("background-color","#ccc")
.attr("stroke-width", 2);
const circles = d3.range(20).map(i => ({
x: Math.random() * (width - radius * 2) + radius,
y: Math.random() * (height - radius * 2) + radius,
}));
svg.selectAll("circle")
.data(circles)
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", radius)
.attr("fill", (d, i) => d3.schemeCategory10[i % 10])
.call(drag);
</script>
</body>
This gives the initial error. Uncaught TypeError: d is undefined Replace the drag definition with
var drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged )
.on("end", dragended);
and it works. Peachy. But if we replace the definition of dragged with:
function dragged(event, d) {
d.x = event.x;
d.y = event.y;
d3.select(this).attr("cx", d.x).attr("cy", d.y);
}
We reveal probably the real underlying error: Uncaught TypeError: this.setAttribute is not a function. So this isn't passing correctly. Same happens if we define:
var drag = d3.drag()
.on("start", dragstarted)
.on("drag", (event,d) => { dragged(event,d) } )
.on("end", dragended);
I know the arrow notation can lead to some this weirdness but at any rate that's not the fix here. If I put the function definition right in the declaration, we get the right this and it works again:
var drag = d3.drag()
.on("start", dragstarted)
.on("drag", function (event,d) {
d3.select(this).attr("cx", d.x = event.x).attr("cy", d.y = event.y);
} )
.on("end", dragended);
Sooo. Why isn't the right this getting passed to dragged()? And how do I fix it? Even though the above works, it would be a little ugly to do this in my code, and at any rate, I want to understand better how passing attributes works in javascript. Hopefully this contained example helps!

Drag points with Lat/Lng inputs

I've been using this example to enable dragging of points. Successful JS fiddle here.
My question is, how do I convert this to run off input data which uses a co-ordinate system based on lat/longs?
I can display/project the points fine, but when I drag it, it pins to the top left corner. DevTools Console returns an error "Error: attribute cx: Expected length, "NaN"." Same returned for attribute cy.
I think it's something to do with the dragged function, but all the permutations I've tried on it have failed.
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight) - 90;
var tile = d3.geo.tile()
.size([width, height]);
var projection = d3.geo.mercator()
.scale((1 << 23) / 2 / Math.PI)
.translate([-width / 2, -height / 2]);
var drag = d3.behavior.drag()
.origin(function (d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
var container = d3.select("body").append("div")
.attr("id", "container")
.style("width", width + "px")
.style("height", height + "px");
var points = container.append("svg")
.attr("id", "points");
var nodes_data_latlng = [{ "lat1": -0.01, "lng1": 0.025 }];
drawnodeslatlng();
function drawnodeslatlng() {
d3.select("#points").selectAll("circle")
.data(nodes_data_latlng)
.enter()
.append("circle")
.attr("cx", function (d) { return projection([d.lng1, d.lat1])[0] })
.attr("cy", function (d) { return projection([d.lng1, d.lat1])[1] })
.attr("r", "10")
.call(drag)
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this)
.attr("cx", d.lng1 = d3.event.x)
.attr("cy", d.lat1 = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
<html>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/d3.geo.tile.v0.min.js"></script>
</body>
</html>
In D3 v3, the .origin method you have here...
var drag = d3.behavior.drag()
.origin(function (d) { return d; })
...requires an object with x and y properties. The API for that quite old and outdated version says:
Frequently the origin accessor is specified as the identity function: function(d) { return d; }. This is suitable when the datum bound to the dragged element is already an object with x and y attributes representing its current position.
Therefore, the easiest solution is simply removing it:
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight) - 90;
var tile = d3.geo.tile()
.size([width, height]);
var projection = d3.geo.mercator()
.scale((1 << 23) / 2 / Math.PI)
.translate([-width / 2, -height / 2]);
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
var container = d3.select("body").append("div")
.attr("id", "container")
.style("width", width + "px")
.style("height", height + "px");
var points = container.append("svg")
.attr("id", "points");
var nodes_data_latlng = [{ "lat1": -0.01, "lng1": 0.025 }];
drawnodeslatlng();
function drawnodeslatlng() {
d3.select("#points").selectAll("circle")
.data(nodes_data_latlng)
.enter()
.append("circle")
.attr("cx", function (d) { return projection([d.lng1, d.lat1])[0] })
.attr("cy", function (d) { return projection([d.lng1, d.lat1])[1] })
.attr("r", "10")
.call(drag)
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this)
.attr("cx", d.lng1 = d3.event.x)
.attr("cy", d.lat1 = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
<html>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/d3.geo.tile.v0.min.js"></script>
</body>
</html>

D3 - dragging multiple elements across groups

I'm trying to achieve a D3 drag behavior, for which I'm not finding an example.
The below code creates a couple of rows with a rectangle, circle and ellipse each, and the drag feature is enabled for the circle.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Drag many</title>
<style media="screen">
.active {
stroke: #000;
stroke-width: 2px;
}
</style>
</head>
<body>
<svg width=1000 height=500></svg>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script type="text/javascript">
var svg=d3.select("svg"),
mainGroup = svg.append("g");
var squareData = [
{x:25, y:23, width:15, height:15},
{x:25, y:63, width:15, height:15}
];
var circleData = [
{cx:60, cy:30, r:10},
{cx:60, cy:70, r:10}
];
var ellipseData = [
{cx:90, cy:30, rx:10, ry:15},
{cx:90, cy:70, rx:10, ry:15}
];
var squareGroup = mainGroup.append("g");
squareGroup.selectAll(null)
.data(squareData)
.enter().append("rect")
.attr("class", (d,i)=>`rectGroup row_${i+1}`)
.attr("x", d=>d.x)
.attr("y", d=>d.y)
.attr("width", d=>d.width)
.attr("height", d=>d.height)
.attr("fill", "blue");
circleGroup = mainGroup.append("g");
circleGroup.selectAll(null)
.data(circleData)
.enter().append("circle")
.attr("class", (d,i)=>`circleGroup row_${i+1}`)
.attr("cx", d=>d.cx)
.attr("cy", d=>d.cy)
.attr("r", d=>d.r)
.attr("fill", "red")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
ellipseGroup = mainGroup.append("g");
ellipseGroup.selectAll(null)
.data(ellipseData)
.enter().append("ellipse")
.attr("class", (d,i)=>`ellipseGroup row_${i+1}`)
.attr("cx", d=>d.cx)
.attr("cy", d=>d.cy)
.attr("rx", d=>d.rx)
.attr("ry", d=>d.ry)
.attr("fill", "green");
function dragstarted(d){
d3.select(this).classed("active", true);
}
function dragended(d){
d3.select(this).classed("active", false);
}
function dragged(d){
//var dx = d3.event.x - d3.select(this).attr("cx")
d3.select(this).attr("cx", d.x = d3.event.x);
// I really need to move all the elements in this row here,
// with the circle being the pivot...
}
</script>
</body>
</html>
I would like to modify the drag behavior of the circle to pull the entire row with it rather than move alone.
How can I achieve this?
It looks like you give each row a class so instead of:
function dragged(d){d3.select(this).attr("cx", d.x = d3.event.x);}
get the class of that row (from the looks of your code it should be the second class) with:var thisRow = d3.select(this).attr("class").split()[1];
.split() will split on spaces which is how getting the class list will spit out its result.
After getting the class move all of that class:
d3.selectAll("." + thisRow).attr("cx", d.x = d3.event.x);
I'm not really sure if there's a better way of finding the row, but here's how I solved it using the concept from #pmkroeker
function dragged(d){
var deltaX = d3.event.x - d3.select(this).attr("cx");
d3.select(this).attr("cx", d.x = d3.event.x); // move the circle
// find the row
var row = d3.select(this).attr("class").split(/\s+/).filter(s=>s.startsWith("row_"))[0];
// grab other elements of the row and move them as well
d3.selectAll("."+row).filter(".rectGroup").attr("x", d=>d.x = d.x+deltaX);
d3.selectAll("."+row).filter(".ellipseGroup").attr("cx", d=>d.cx = d.cx+deltaX);
}

D3js drag&drop an object and detect where it is dropped.

I am working on dragging and dropping svg using d3js. There are two problems and I think they are related to each other.
When the circle is dropped it has to detect that it was dropped into the rectangle. Some of the examples that I have looked at uses x and y coordinates of the mouse, but I don't fully understand it.
Another problem is that the circle appears behind the rectangle. Is there a way to bring it to the front when the circle is moving around without changing the order of where the circle and rectangle are created i.e(create circle first and then rectangle).
var width = window.innerWidth,
height = window.innerHeight;
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
//create circle and space evenly
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var circle = d3.select("svg")
.append("circle")
.attr("cx", 50)
.attr("cy", 30)
.attr("r", 15)
.attr("transform", "translate(0,0)")
.style("stroke", "black")
.call(drag);
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
}
function dragged(d) {
d3.select(this).attr("transform", "translate(" + [d3.event.x, d3.event.y] + ")");
}
function dragended(d) {
d3.event.sourceEvent.stopPropagation();
// here would be some way to detect if the circle is dropped inside the rect.
}
var ellipse = svg.append("rect")
.attr("x", 150)
.attr("y", 50)
.attr("width", 50)
.attr("height", 140)
.attr("fill", "green");
<script src="https://d3js.org/d3.v3.min.js"></script>
Any help is appreciated.
Updated to still include the bounding client rectangle, but iterate through any number of rectangles that exist. New Fiddle here.
Here's my solution to the problem. I used a great little "moveToBack" helper function seen here to move the rect to the back without changing the order in which it appears.
To get the positions of the circle and rectangle, I made heavy use of the vanilla js getBoundingClientRect() method. You can see all this together in this JS Fiddle.
var width = window.innerWidth,
height = window.innerHeight;
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
//create circle and space evenly
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var circle = d3.select("svg")
.append("circle")
.attr("r", 15)
.attr("transform", "translate(50,30)")
.style("stroke", "black")
.attr("id", "circle")
.call(drag);
d3.selection.prototype.moveToBack = function() {
return this.each(function() {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
var rect = svg.append("rect")
.attr("x", 150)
.attr("y", 50)
.attr("width", 50)
.attr("height", 140)
.attr("fill", "green")
.attr("id", "rect")
.moveToBack();
var rect2 = svg.append("rect")
.attr("x", 350)
.attr("y", 50)
.attr("width", 50)
.attr("height", 140)
.attr("fill", "green")
.attr("id", "rect")
.moveToBack();
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
}
function dragged(d) {
d3.select(this).attr("transform", "translate(" + d3.event.x + "," + d3.event.y + ")");
}
function dragended(d) {
// Define boundary
var rects = document.querySelectorAll("rect");
for (var i = 0; i < rects.length; i++) {
var rectDimensions = rects[i].getBoundingClientRect();
var xmin = rectDimensions.x;
var ymin = rectDimensions.y;
var xmax = rectDimensions.x + rectDimensions.width;
var ymax = rectDimensions.y + rectDimensions.height;
// Get circle position
var circlePos = document.getElementById("circle").getBoundingClientRect();
var x1 = circlePos.x;
var y1 = circlePos.y;
var x2 = circlePos.x + circlePos.width;
var y2 = circlePos.y + circlePos.height;
if(x2 >= xmin && x1 <= xmax && y2 >= ymin && y1 <= ymax) {
rects[i].setAttribute("fill", "red");
} else {
rects[i].setAttribute("fill", "green");
}
}
d3.event.sourceEvent.stopPropagation();
}

Extending the object boundary for dragging in d3

I am playing around with a great tutorial from here http://ssun.azurewebsites.net/creating-a-draggable-object-in-d3/. What is the best way to extend the active area of the circle for dragging on click? I see three possible solutions:
create a complex object that has two circles, one visible and one invisible, but I am not sure if the invisible circle can be clicked on. Maybe 100% transparent.
Extend the active area of the mouse cursor(if that is even possible)
Extend the active area of the circle beyond visual part.
Unfortunately, I do not know how to do any of those things. Any help would be appreciated.
I like two circle approach but I'd group them in a g element. The dragging then works on the g element and the second circle is simply to expand the g:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
</head>
<body>
<script>
var boxWidth = 600;
var boxHeight = 400;
var box = d3.select('body')
.append('svg')
.attr('class', 'box')
.attr('width', boxWidth)
.attr('height', boxHeight);
var drag = d3.behavior.drag()
.on('dragstart', function() {
circle.style('fill', 'red');
})
.on('drag', function() {
d3.select(this)
.attr('transform', function(d) {
return "translate(" + d3.event.x + "," + d3.event.y + ")";
});
})
.on('dragend', function() {
circle.style('fill', 'black');
});
var dragCircles = box.selectAll('.draggableCircle')
.data([{
x: (boxWidth / 2),
y: (boxHeight / 2),
r: 25
}])
.enter()
.append('g')
.attr('class', 'draggableCircle')
.attr('transform', function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.style('cursor', 'crosshair')
.call(drag);
dragCircles.append("circle")
.attr('r', function(d){
return d.r * 3;
})
.style('fill', 'transparent');
var circle = dragCircles.append("circle")
.attr('r', function(d) {
return d.r;
})
.style('fill', 'black');
</script>
</body>
</html>

Categories