Related
I would like to create an application like scratch or node-red, with D3.js, by this I mean create some svg elements by clicking on a 'button list' to create an element and then drag them over an area to arrange them.
This idea is working with my code below. I can click to create shapes (svg group). Once created, I can click on them (AGAIN) and drag it over svg area.
But, I want to mimic the behavior of same apps node-red and scratch, by dragging the new svg element with the same click used to create it. Sparing a click, in one word. But I don't know how to start drag behavior programmatically on the element created. Here is my working code.
var svg = d3.select("body").append("svg")
.attr("width", 1500)
.attr("height", 800);
addButton(svg, 'ADD');
function addShape(svg, x, y) {
var dotContainer = svg.append("g")
.attr("class", "dotContainer")
.datum({
x: x,
y: y
})
.attr("transform", function(d) {
return 'translate(' + d.x + ' ' + d.y + ')';
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var text = dotContainer.append("text")
.datum({
x: 20,
y: 20
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.text('Title');
var rectangle = dotContainer.append("rect")
.attr("width", 200)
.attr("height", 100)
.attr("x", 0)
.attr("y", 0)
.attr('style', "opacity:1;fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:5;stroke-opacity:1")
.attr("ry", 8);
return dotContainer;
}
function dragstarted(d) {
let xCoord = d3.event.dx - d3.select(this).attr('x')
let yCoord = d3.event.dy - d3.select(this).attr('y')
}
function dragged(d) {
d3.select(this).select("text").text(d.x + ';' + d.y);
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this).attr("transform", function(d, i) {
return "translate(" + [d.x, d.y] + ")"
});
}
function dragended(d) {
d3.select(this).attr("transform", function(d, i) {
return "translate(" + [d.x, d.y] + ")"
});
}
function addButton(area, title) {
var group = area.append("g");
group.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 50)
.attr('style', 'fill:rgb(255,0,0);stroke-width:1;stroke:rgb(200,200,200)');
group.append("text")
.attr('x', 20)
.attr('y', 20)
.text(title);
group.on('mousedown', function() {
var grp = addShape(area, 0, 0);
//START DRAG ON grp HERE ???
});
}
<script src="https://d3js.org/d3.v5.min.js"></script>
So, my issue is here that I can't figure out how to call dragstarted() from outside of svg group dotContainer, since dragstarted use this and d, which refers to the svg group. Or use a complete other way to achieve this? I am lost here....
Thanks,
When in doubt, you can always reach back to vanilla JavaScript. In this case, you can dispatch a custom MouseDown event using the d3.event object as the attribute dictionary, essentially cloning the element.
Then, the MouseMove events take over and are processed seamlessly:
var svg = d3.select("body").append("svg")
.attr("width", 1500)
.attr("height", 800);
addButton(svg, 'ADD');
function addShape(svg, x, y) {
var dotContainer = svg.append("g")
.attr("class", "dotContainer")
.datum({
x: x,
y: y
})
.attr("transform", function(d) {
return 'translate(' + d.x + ' ' + d.y + ')';
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var text = dotContainer.append("text")
.datum({
x: 20,
y: 20
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.text('Title');
var rectangle = dotContainer.append("rect")
.attr("width", 200)
.attr("height", 100)
.attr("x", 0)
.attr("y", 0)
.attr('style', "opacity:1;fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:5;stroke-opacity:1")
.attr("ry", 8);
return dotContainer;
}
function dragstarted(d) {
let xCoord = d3.event.dx - d3.select(this).attr('x')
let yCoord = d3.event.dy - d3.select(this).attr('y')
}
function dragged(d) {
d3.select(this).select("text").text(d.x + ';' + d.y);
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this).attr("transform", function(d, i) {
return "translate(" + [d.x, d.y] + ")"
});
}
function dragended(d) {
d3.select(this).attr("transform", function(d, i) {
return "translate(" + [d.x, d.y] + ")"
});
}
function addButton(area, title) {
var group = area.append("g");
group.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 50)
.attr('style', 'fill:rgb(255,0,0);stroke-width:1;stroke:rgb(200,200,200)');
group.append("text")
.attr('x', 20)
.attr('y', 20)
.text(title);
group.on('mousedown', function() {
var grp = addShape(area, 0, 0);
grp.node().dispatchEvent(new MouseEvent(
"mousedown",
d3.event
));
});
}
<script src="https://d3js.org/d3.v5.js"></script>
Yo could listen for a mousedown on the button used to create the new shape. In the event listener, you create a new shape and create a new mousedown event which you dispatch immediately on the new element. This new mousedown event will trigger the drag behavior, triggering the drag-start listener once and the drag listener continuously until the mouse is raised. This could look like:
select.on("mousedown", function(event,d) {
// create some new shape:
var aNewShape = container.append("shape")
.attr(...)
....
// create a new event and dispatch it on the new shape
var e = document.createEvent("MouseEvents");
e.initMouseEvent("mousedown", true,true,window,0,0,0,event.x,event.y)
aNewShape.node().dispatchEvent(e)
})
Which could look something like:
var svg = d3.select("body")
.append("svg")
.attr("width",400)
.attr("height", 300);
var data = [
{shape: d3.symbolCross, y: 0, cy: 25, cx: 25},
{shape: d3.symbolWye, y: 60, cy: 85, cx: 25 },
{shape: d3.symbolDiamond, y: 120, cy: 145, cx: 25}
]
// Add some buttons:
var g = svg.selectAll("null")
.data(data)
.enter()
.append("g")
.attr("transform",function(d,i) {
return "translate("+[0,d.y]+")";
})
g.append("rect")
.attr("width", 50)
.attr("height", 50)
.attr("fill", "#ddd");
g.append("path")
.attr("d", function(d) { return d3.symbol().type(d.shape).size(100)(); })
.attr("transform","translate(25,25)")
.attr("fill", "#aaa");
// Some sort of drag function
var drag = d3.drag()
.on("drag", function(event,d) {
d.x = event.x;
d.y = event.y;
d3.select(this).attr("transform", "translate("+[d.x,d.y]+")");
})
.on("start", function() {
d3.select(this).transition()
.attr("fill","steelblue")
.duration(1000);
})
// Mouse down event:
g.on("mousedown", function(event,d) {
var shape = svg.append("path")
.datum({type:d.shape,x:d.cx,y:d.cy})
.attr("d", d3.symbol().type(d.shape).size(300)())
.attr("transform", function(d) { return "translate("+[d.x,d.y]+")" })
.attr("fill","black")
.call(drag);
var e = document.createEvent("MouseEvents");
e.initMouseEvent("mousedown", true,true,window,0,0,0,event.x,event.y)
shape.node().dispatchEvent(e);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
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>
I have a SVG and I want to apply rotation transformation on it but not just with the mouse drag, instead with a mouse drag while pressing the alt key. I have tried to implement something but things donot seem to work as I want. Below is what i have implemented. How can I get the SVG rotated on dragging the mouse while pressing the alt key?
var width = 590, height = 400;
var svg = d3.select("#sketch4").append("svg")
.attr("width", width)
.attr("height", height)
//.call(d3.zoom().on("zoom", zoomed))
.call(d3.drag().on('drag', dragged))
.append("g")
var rect = svg.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("width", 60)
.attr("height", 30)
.attr("fill", "green")
function dragged() {
if (d3.event.sourceEvent.altKey){
var me = sketchSVG.node()
var x1 = me.getBBox().x + me.getBBox().width/2;
var y1 = me.getBBox().y + me.getBBox().height/2;
sketchSVG.attr("transform","rotate("+d3.event.y+","+x1+","+y1+")" );
metricSVG.attr("transform","rotate("+d3.event.y+","+x1+","+y1+")" );
}
};
You need to use d3.event.sourceEvent.x and d3.event.sourceEvent.y. Changing that should get your code working.
To move/zoom your element, create and else condition and just use sketchSVG.attr("transform",d3.event.transform) for zooming and translating the element.
var width = 590,
height = 400;
var svg = d3.select("#sketch4").append("svg")
.attr("width", width)
.attr('id', 'sketchSvg')
.attr("height", height)
.append("g")
d3.select('#sketchSvg')
.call(d3.zoom().on("zoom", dragged))
var rect = svg.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("width", 60)
.attr("height", 30)
.attr("fill", "green")
function dragged(d) {
if (d3.event.sourceEvent.altKey && d3.event.sourceEvent.type == 'mousemove') {
var me = svg.node()
var x1 = me.getBBox().x + me.getBBox().width / 2;
var y1 = me.getBBox().y + me.getBBox().height / 2;
svg.attr("transform", "rotate(" + d3.event.sourceEvent.y + "," + x1 + "," + y1 + ")");
}
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id='sketch4'></div>
Here's one on JSFiddle
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();
}
I am able to append a rectangle to group on drag drop but it is not working properly. If I drag and drop multiple rectangles on to group, the rectangles are dislocated at different location. I am sure there must be some thing wrong.
Demo: http://jsfiddle.net/wqvLLbLa/
code:
var svgContainer = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 803);
var rect = svgContainer.append("rect")
.attr("x", 10)
.attr("y", 50)
.attr("width", 51)
.attr("height", 41)
.attr("rx", 10)
.attr("stroke-width", 2)
.attr("stroke", "#7E7E7E")
.style('cursor', 'move')
.style("fill", "white");
function moveRect() {
d3.select(this)
.attr('x', d3.event.x)
.attr('y', d3.event.y);
}
var dragGroup = d3.behavior.drag()
.origin(function () {
var g = this;
return {x: d3.transform(g.getAttribute("transform")).translate[0],
y: d3.transform(g.getAttribute("transform")).translate[1]};
})
.on("drag", function (d, i) {
g = this;
translate = d3.transform(g.getAttribute("transform")).translate;
console.log(translate);
x = d3.event.dx + translate[0],
y = d3.event.dy + translate[1];
d3.select(g).attr("transform", "translate(" + x + "," + y + ")");
d3.event.sourceEvent.stopPropagation();
});
var group = svgContainer.append("g")
.attr("id", "mygroup")
.call(dragGroup)
.style('cursor', 'move')
.attr("transform", "translate(20, 20)");
group.append("rect")
.attr("x", 250)
.attr("y", 250)
.attr("width", 151)
.attr("height", 141)
.attr("rx", 10)
.attr("stroke-width", 2)
.attr("stroke", "#7E7E7E")
.style("fill", "white");
var circleDrag = d3.behavior.drag()
.origin(function ()
{
var t = d3.select(this);
return {x: t.attr("cx"), y: t.attr("cy")};
})
var rectDrag = d3.behavior.drag()
.origin(function ()
{
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
})
.on('dragend', function (d) {
var mouseCoordinates = d3.mouse(this);
var groupTransform = d3.transform(group.attr("transform"));
var groupX = groupTransform.translate[0];
var groupY = groupTransform.translate[1];
var rect = group.select("rect");
var rectX = +rect.attr("x");
var rectY = +rect.attr("y");
var rectWidth = +rect.attr("width");
var rectHeight = +rect.attr("height");
if (mouseCoordinates[0] > groupX + rectX
&& mouseCoordinates[0] < groupX + rectX + rectWidth
&& mouseCoordinates[1] > groupY + rectY
&& mouseCoordinates[1] < groupY + rectY + rectHeight) {
//Append new element
var newRect = d3.select("g").append("rect")
.classed("drg", true)
.attr("x", 100)
.attr("y", 100)
.attr("rx", 10)
.attr("width", 51)
.attr("height", 41)
.attr("x", mouseCoordinates[0])
.attr("y", mouseCoordinates[1])
.style("fill", "white")
.style("stroke-width", 2)
.style("stroke", "#CDB483");
}
else
{
var newRect = d3.select("svg").append("rect")
.classed("drg", true)
.attr("x", 100)
.attr("y", 100)
.attr("rx", 10)
.attr("width", 51)
.attr("height", 41)
.attr("x", mouseCoordinates[0])
.attr("y", mouseCoordinates[1])
.style("fill", "white")
.style("stroke-width", 2)
.style("stroke", "#CDB483")
.call(
d3.behavior.drag()
.on('drag', moveRect).origin(function () {
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
}));
}
});
rect.call(rectDrag);
Responding to the update question - as in the comments.
The reason for the duplicates is because you are appending a new element to the targetG after your bounds check, instead of targetCircle.
The easiest way to fix this would be to simply remove targetCircle after appending the new circle like so
targetCircle.remove();
Fiddle - http://jsfiddle.net/afmLhofL/