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>
Related
I'm try to figure out how we can add tooltip for feature that is behind some other features.
For example in picture there are some circles overlapped to each other I want get information about both features/circles if mouseover on overlapped area.
Code and tooltip example is attached below.
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var circles = d3.range(20).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.text("a simple tooltip");
svg.selectAll("circle")
.data(circles)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.on("mouseover", function(d){
return tooltip.style("visibility", "visible");
})
.on("mousemove", function(d){
return tooltip
.html("Radius: " + d.x)
.style("top", (event.pageY-10)+"px").style("left", (event.pageX+10)+"px");
})
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");})
.style("fill", function(d, i) { return color(i); })
.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("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("active", false);
}
svg {
border: 1px solid black;
}
.active {
stroke: #000;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>
<svg width="960" height="500"></svg>
This will calculate the mouse position's distance from all the other circles to test if the mouse is over more then one circle:
<!DOCTYPE html>
<html>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>
<svg width="500" height="500"></svg>
<script>
var svg = d3.select('svg'),
width = +svg.attr('width'),
height = +svg.attr('height'),
radius = 32;
var circles = d3.range(20).map(function () {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius),
};
});
var color = d3.scaleOrdinal().range(d3.schemeCategory20);
var tooltip = d3
.select('body')
.append('div')
.style('position', 'absolute')
.style('z-index', '10')
.style('visibility', 'hidden')
.text('a simple tooltip');
var circles = svg
.selectAll('circle')
.data(circles)
.enter()
.append('circle')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
.attr('r', radius)
.on('mouseover', function (d) {
return tooltip.style('visibility', 'visible');
})
.on('mousemove', function (d) {
var txt = 'X: ' + d.x,
m = d3.mouse(this);
circles.each(function(d1){
// if not circle with "mouseover"
if (d.x !== d1.x || d.y !== d1.y)
{
// check distance
if (Math.sqrt((d1.x - m[0])**2 + (d1.y - m[1])**2) <= 32)
{
// add to tooltip
txt += '<br/> X: ' + d1.x;
}
}
});
return tooltip
.html(txt)
.style('top', event.pageY - 10 + 'px')
.style('left', event.pageX + 10 + 'px');
})
.on('mouseout', function (d) {
return tooltip.style('visibility', 'hidden');
})
.style('fill', function (d, i) {
return color(i);
})
.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('cx', (d.x = d3.event.x))
.attr('cy', (d.y = d3.event.y));
}
function dragended(d) {
d3.select(this).classed('active', false);
}
</script>
</body>
</html>
I want to make a draggable <g> element containing a <circle> and a <text> but I failed.
Now I have this code:
var margin = {top: -5, right: -5, bottom: -5, left: -5},
width = 1960 - margin.left - margin.right,
height = 1500 - margin.top - margin.bottom;
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var dotContainer = svg.append("g")
.attr("class", "dotContainer")
.datum({x:220, y:120})
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.call(drag);
var dot = dotContainer.append("circle")
.attr("class", "dot")
.datum({x:220, y:120})
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5).call(drag);
var text = dotContainer.append("text")
.datum({x:220, y:120})
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.text('Title');
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<circle> element is perfectly draggable, ok. But I need to apply dragging to the whole <g> element to drag my circle together with the <text> element.
How can I do this?
When I apply call(drag) to dotContainer like this
dotContainer.call(drag);
then nothing works.
You should rewrite your dotContainer variable this way:
var dotContainer = svg.append("g")
.attr("class", "dotContainer")
.datum({x:220, y:120})
.attr("transform", function(d) { return 'translate(' + d.x + ' '+ d.y + ')'; })
.call(drag);
Remove .call(drag) for dot variable and rewrite dragged function like this:
function dragged(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d.x,d.y ] + ")"
});
}
Thus we use transform attribute for an initial container position and for the position update during the dragging.
Check demo in the hidden snippet:
var margin = {top: -5, right: -5, bottom: -5, left: -5},
width = 1960 - margin.left - margin.right,
height = 1500 - margin.top - margin.bottom;
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var dotContainer = svg.append("g")
.attr("class", "dotContainer")
.datum({x:20, y:20})
.attr("transform", function(d) { return 'translate(' + d.x + ' '+ d.y + ')'; })
.call(drag);
var dot = dotContainer.append("circle")
.attr("class", "dot")
.datum({x:20, y:20})
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
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');
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
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).classed("dragging", false);
}
.dotContainer {
cursor: move;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
I am new to D3 and trying to dynamically update the chart if the source json is modified. But I am not able to achieve this.
Please check this plunkr
Js:
var width = 500,
height = 500,
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.sqrt()
.range([0, radius]);
var color = d3.scale.category10();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ") rotate(-90 0 0)");
var partition = d3.layout.partition()
.value(function(d) {
return d.size;
});
var arc = d3.svg.arc()
.startAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x)));
})
.endAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
})
.innerRadius(function(d) {
return Math.max(0, y(d.y));
})
.outerRadius(function(d) {
return Math.max(0, y(d.y + d.dy));
});
//d3.json("/d/4063550/flare.json", function(error, root) {
var root = initItems;
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
var path = g.append("path")
.attr("d", arc)
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.on("click", click)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
});
//.append("text")
var text = g.append("text")
.attr("x", function(d) {
return y(d.y);
})
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
function computeTextRotation(d) {
var angle = x(d.x + d.dx / 2) - Math.PI / 2;
return angle / Math.PI * 180;
}
function click(d) {
console.log(d)
// fade out all text elements
if (d.size !== undefined) {
d.size += 100;
};
text.transition().attr("opacity", 0);
path.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.attr("transform", function() {
return "rotate(" + computeTextRotation(e) + ")"
})
.attr("x", function(d) {
return y(d.y);
});
}
});
} //});
// Word wrap!
var insertLinebreaks = function(t, d, width) {
alert(0)
var el = d3.select(t);
var p = d3.select(t.parentNode);
p.append("g")
.attr("x", function(d) {
return y(d.y);
})
// .attr("dx", "6") // margin
//.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
//p
.append("foreignObject")
.attr('x', -width / 2)
.attr("width", width)
.attr("height", 200)
.append("xhtml:p")
.attr('style', 'word-wrap: break-word; text-align:center;')
.html(d.name);
alert(1)
el.remove();
alert(2)
};
//g.selectAll("text")
// .each(function(d,i){ insertLinebreaks(this, d, 50 ); });
d3.select(self.frameElement).style("height", height + "px");
// Interpolate the scales!
function arcTween(d) {
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i ? function(t) {
return arc(d);
} : function(t) {
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
function arcTweenUpdate(a) {
console.log(path);
var _self = this;
var i = d3.interpolate({ x: this.x0, dx: this.dx0 }, a);
return function(t) {
var b = i(t);
console.log(window);
_self.x0 = b.x;
_self.dx0 = b.dx;
return arc(b);
};
}
setTimeout(function() {
path.data(partition.nodes(newItems))
.transition()
.duration(750)
.attrTween("d", arcTweenUpdate)
}, 2000);
In addition to what #Cyril has suggested about removing the following line:
d3.select(self.frameElement).style("height", height + "px");
I made further modifications in your fiddle: working fiddle
The idea used here is to add a function updateChart which takes the items and then generate the chart:
var updateChart = function (items) {
// code to update the chart with new items
}
updateChart(initItems);
setTimeout(function () { updateChart(newItems); }, 2000);
This doesn't use the arcTweenUpdate function you have created but I will try to explain the underlying concept:
First, you will need to JOIN the new data with your existing data:
// DATA JOIN - Join new data with old elements, if any.
var gs = svg.selectAll("g").data(partition.nodes(root));
then, ENTER to create new elements if required:
// ENTER
var g = gs.enter().append("g").on("click", click);
But, we also need to UPDATE the existing/new path and text nodes with new data:
// UPDATE
var path = g.append("path");
gs.select('path')
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
//.on("click", click)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
})
.transition().duration(500)
.attr("d", arc);
var text = g.append("text");
gs.select('text')
.attr("x", function(d) {
return y(d.y);
})
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
and, after everything is created/updated remove the g nodes which are not being used i.e. EXIT:
// EXIT - Remove old elements as needed.
gs.exit().transition().duration(500).style("fill-opacity", 1e-6).remove();
This whole pattern of JOIN + ENTER + UPDATE + EXIT is demonstrated in following articles by Mike Bostock:
General Update Pattern - I
General Update Pattern - II
General Update Pattern - III
In side the fiddle the setTimeout is not running because:
d3.select(self.frameElement).style("height", height + "px");
You will get Uncaught SecurityError: Failed to read the 'frame' property from 'Window': Blocked a frame with origin "https://fiddle.jshell.net" from accessing a frame with origin and the setTimeout never gets called.
So you can remove this line d3.select(self.frameElement).style("height", height + "px"); just for the fiddle.
Apart from that:
Your timeout function should look like this:
setTimeout(function() {
//remove the old graph
svg.selectAll("*").remove();
root = newItems;
g = svg.selectAll("g")
.data(partition.nodes(newItems))
.enter().append("g");
/make path
path = g.append("path")
.attr("d", arc)
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.on("click", click)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
});
//make text
text = g.append("text")
.attr("x", function(d) {
return y(d.y);
})
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
}
working fiddle here
for the enter() and transitions to work you need to give d3 a way to identify each item in your data. the .data() function has a second parameter that lets you return something to use as an id. enter() will use the id to decide whether the object is new.
try changing
path.data(partition.nodes(newItems))
.data(partition.nodes(root));
to
path.data(partition.nodes(newItems), function(d){return d.name});
.data(partition.nodes(root), function(d){return d.name});
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/
I'm working on a force layout graph that displays relationships of writers. Since there are so many, I tried to implement zooming and dragging. Zooming works fine (with one exception), but when I drag a node it also drags the background. I tried following Mike Bostock's directions here and the StackOverflow question paired with it, but it still won't work. I based most of the code for the graph on this, which works beautifully, but since he used an older version of d3, his dragging breaks in the new version. (I can't just use the older version of d3 because I have some other parts of the graph not shown here that work only with the newer version.)
I think the problem has something to do with my grouping of SVG objects, but I also can't figure out what I'm doing wrong there. This also brings in the one zooming problem; when I zoom in or pan around, the legend also moves and zooms in. If there's an easy fix to make it stay still and sort of "hover" above the graph, that would be great.
I'm very new to coding, so I'm probably making really stupid mistakes, but any help would be appreciated.
Fiddle.
var graphData = {
nodes: [
{
id:0,
name:"Plotinus"
},
{
id:1,
name:"Iamblichus"
},
{
id:2,
name:"Porphyry"
}
],
links: [
{
relationship:"Teacher/student",
source:0,
target:1
},
{
relationship:"Enemies",
source:0,
target:2
},
{
relationship:"Family",
source:1,
target:2
}
]
};
var linkColor = d3.scale.category10(); //Sets the color for links
var drag = d3.behavior.drag()
.on("dragstart", function() { d3.event.sourceEvent.stopPropagation(); })
.on("drag", function(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
});
var w = 300,
h = 300;
var vis = d3.select(".graph")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.attr("pointer-events", "all")
.append('svg:g')
.call(d3.behavior.zoom().on("zoom", redraw))
.append('svg:g');
vis.append('svg:rect')
.attr('width', w)
.attr('height', h)
.attr('fill', 'rgba(1,1,1,0)');
function redraw() {
vis.attr("transform","translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"); }
var force = d3.layout.force()
.gravity(.6)
.charge(-600)
.linkDistance( 60 )
.size([w, h]);
var svg = d3.select(".text").append("svg")
.attr("width", w)
.attr("height", h);
var link = vis.selectAll("line")
.data(graphData.links)
.enter().append("line")
.style("stroke", function(d) { return linkColor(d.relationship); })
.style("stroke-width", 1)
.attr("class", "connector");
var node = vis.selectAll("g.node")
.data(graphData.nodes)
.enter().append("svg:g")
.attr("class","node")
.call(force.drag);
node.append("svg:circle")
.attr("r", 10) //Adjusts size of nodes' radius
.style("fill", "#ccc");
node.append("svg:text")
.attr("text-anchor", "middle")
.attr("fill","black")
.style("pointer-events", "none")
.attr("font-size", "9px")
.attr("font-weight", "100")
.attr("font-family", "sans-serif")
.text( function(d) { return d.name;} );
// Adds the legend.
var legend = vis.selectAll(".legend")
.data(linkColor.domain().slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(-10," + i * 20 + ")"; });
legend.append("rect")
.attr("x", w - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", linkColor);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.attr("class", "legendText")
.style("text-anchor", "end")
.text(function(d) { return d; });
force
.nodes(graphData.nodes)
.links(graphData.links)
.on("tick", tick)
.start();
function tick() {
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")";});
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
I think I figured it out.
I had to combine the instructions from here and here, which was sort of already answered in the answer I linked.
My old way, grabbed from the first example, looked like this:
var drag = d3.behavior.drag()
.on("dragstart", function() { d3.event.sourceEvent.stopPropagation(); })
.on("drag", function(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
});
The problem was that I was focusing on d3.behavior.drag() instead of force.drag, which I think Stephen Thomas was trying to tell me. It should look like this:
//code code code//
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
//code code code//
var drag = force.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
You can use the drag() method of the force object instead of creating a separate drag behavior. Something like:
node.call(force.drag);
or, equivalently,
force.drag(node);
A complete example is available at http://bl.ocks.org/sathomas/a7b0062211af69981ff3
Here is what is working for me:
const zoom = d3.behavior.zoom()
.scaleExtent([.1, 10])
.on('zoom', zoomed);
const force = d3.layout.force()
.(...more stuff...);
const svg = d3.select('.some-parent-div')
.append('svg')
.attr('class', 'graph-container')
.call(zoom);
const mainGroup = svg.append('g');
var node = mainGroup.selectAll('.node');
node.enter()
.insert('g')
.attr('class', 'node')
.call(force.drag)
.on('mousedown', function(){
// line below is the key to make it work
d3.event.stopPropagation();
})
.(...more stuff...);
function zoomed(){
force.stop();
const canvasTranslate = zoom.translate();
mainGroup.attr('transform', 'translate('+canvasTranslate[0]+','+canvasTranslate[1]+')scale(' + zoom.scale() + ')');
force.resume();
}
With your code, the node can be dragged but when you drag a node other nodes will move too. I come up this to stop rest of nodes and just let you finished dragging then re-generated the whole graph
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("fixed", d.fixed = true);
}
function dragged(d) {
force.stop();
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
tick();
}
function dragended(d) {
force.resume();
}