D3.js static force layout not working with path? - javascript

Im trying change this example https://bl.ocks.org/mbostock/1667139 to use path instead of line, but its not working.
Im try use own tick function like this:
function tick() {
link.attr("d", function(d) {
var x1 = d.source.x,
y1 = d.source.y,
x2 = d.target.x,
y2 = d.target.y,
dx = x2 - x1,
dy = y2 - y1,
dr = Math.sqrt(dx * dx + dy * dy),
// z uzla do ineho uzla
drx = dr,
dry = dr,
xRotation = 0,
largeArc = 0,
sweep = 1;
//do sameho seba
if ( x1 === x2 && y1 === y2 ) {
xRotation = -45;
largeArc = 1;
drx = 30;
dry = 30;
x2 = x2 + 1;
y2 = y2 + 1;
}
return "M" + x1 + "," + y1 + "A" + drx + "," + dry + " " + xRotation + "," + largeArc + "," + sweep + " " + x2 + "," + y2;
});
}
I dont know, if i missing something or static force layout just cant use path.
Force layout with path working normaly

From the docs (bolding mine):
simulation.tick()
Increments the current alpha by (alphaTarget - alpha) × alphaDecay;
then invokes each registered force, passing the new alpha; then
decrements each node’s velocity by velocity × velocityDecay; lastly
increments each node’s position by velocity.
This method does not dispatch events; events are only dispatched by
the internal timer when the simulation is started automatically upon
creation or by calling simulation.restart. The natural number of ticks
when the simulation is started is ⌈log(alphaMin) / log(1 -
alphaDecay)⌉; by default, this is 300.
This method can be used in conjunction with simulation.stop to compute
a static force layout. For large graphs, static layouts should be
computed in a web worker to avoid freezing the user interface.
Since it doesn't dispatch events your tick function is never called or used. Instead, just replace the line and set your path up once:
<!DOCTYPE html>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var n = 100,
nodes = d3.range(n).map(function(i) {
return {
index: i
};
}),
links = d3.range(n).map(function(i) {
return {
source: i,
target: (i + 3) % n
};
});
var simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-80))
.force("link", d3.forceLink(links).distance(20).strength(1).iterations(10))
.force("x", d3.forceX())
.force("y", d3.forceY())
.stop();
var loading = svg.append("text")
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.text("Simulating. One moment please…");
// Use a timeout to allow the rest of the page to load first.
d3.timeout(function() {
loading.remove();
// See https://github.com/d3/d3-force/blob/master/README.md#simulation_tick
for (var i = 0, n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); i < n; ++i) {
simulation.tick();
}
g.append("g")
.attr("stroke", "#000")
.attr("stroke-width", 1.5)
.selectAll("line")
.data(links)
.enter().append("path")
.attr("d", function(d) {
var x1 = d.source.x,
y1 = d.source.y,
x2 = d.target.x,
y2 = d.target.y,
dx = x2 - x1,
dy = y2 - y1,
dr = Math.sqrt(dx * dx + dy * dy),
// z uzla do ineho uzla
drx = dr,
dry = dr,
xRotation = 0,
largeArc = 0,
sweep = 1;
//do sameho seba
if (x1 === x2 && y1 === y2) {
xRotation = -45;
largeArc = 1;
drx = 30;
dry = 30;
x2 = x2 + 1;
y2 = y2 + 1;
}
return "M" + x1 + "," + y1 + "A" + drx + "," + dry + " " + xRotation + "," + largeArc + "," + sweep + " " + x2 + "," + y2;
});
g.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 4.5);
});
</script>
Response to comments:
To append a circle and text as a "node", I would create a g, position that and then put the circle and text in it:
var g = node
.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("transform", function(d){
return "translate(" + d.x + "," + d.y + ")";
});
g.append("circle")
.attr("class", "node")
.attr("stroke", "#fff")
.attr("r", 28);
g.append("text")
.text("test");

Related

d3 drag blurring/jittery handle

This is on d3 v4.
I'm trying to create an expandable rectangle area, with bounds (sort of a constrained d3-brush). I add a handle which shows up on mouseover.
var rectHeight = 80, rectWidth = 100, maxWidth = 200;
var svg = d3.select("svg");
var brect = svg.append("g")
.attr("id", "brect");
brect.append("rect")
.attr("id", "dataRect")
.attr("width", rectWidth)
.attr("height", rectHeight)
.attr("fill", "green");
var handleResizeGroup = brect.append("g")
.attr("id", "handleResizeGroup")
.attr("transform", `translate(${rectWidth})`)
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded));
function dragStarted() {
d3.select(this.previousSibling).classed("active", true);
}
function dragEnded() {
d3.select(this.previousSibling).classed("active", false);
}
function dragged(d) {
var h = d3.select(this);
var r = d3.select(this.previousSibling);
var currWidth = r.attr("width");
var t = (d3.event.x >= 0 && d3.event.x <= maxWidth) ? d3.event.x : currWidth;
r.attr("width", t);
h.attr("transform", `translate(${t})`)
}
handleResizeGroup.append("path")
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.attr("stroke", "grey")
.attr("cursor", "ew-resize")
.attr("d", resizePath);
handleResizeGroup.append("rect")
.attr("id", "resizeRect")
.attr("width", "8")
.attr("fill-opacity", 0)
.attr("cursor", "ew-resize")
.attr("height", rectHeight)
//.attr("pointer-events", "all")
.on("mouseover", function(){
d3.select(this.previousSibling)
.attr("stroke-opacity", "100%");
})
.on("mouseout", function() {
d3.select(this.previousSibling)
.attr("stroke-opacity", "0");
});
function resizePath(d) {
var e = 1,
x = e ? 1 : -1,
y = rectHeight / 3;
return "M" + (.5 * x) + "," + y
+ "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6)
+ "V" + (2 * y - 6)
+ "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y)
+ "Z"
+ "M" + (2.5 * x) + "," + (y + 8)
+ "V" + (2 * y - 8)
+ "M" + (4.5 * x) + "," + (y + 8)
+ "V" + (2 * y - 8);
}
rect.active {
stroke-width: 1;
stroke: rgb(0,0,0);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width=1200 height=500></svg>
I'm noticing 2 issues
When I drag the handle, I see a jitter in the handle itself (presumably because handle is shown only on mouseover?)
If I drag the mouse too fast - say to the left, the rectangle does not catch-up
Can someone help to understand what's going on, and how to fix these?
Thank you!
For your first issue, add .attr('pointer-events', 'none') to the greenrect`. the handle is jittering because your cursor moves slightly faster than the handle does- so you're constantly mousing-out of and then mousing-in to the handle as you move and it catches up.
I don't really see what you're doing with resizePath. Is that adding the grey stroke around the rect? Why not just add/remove a stroke? Your second issue may be due to constantly resizing that path.

d3 js triangle in svg path

i must create 30 triangles that move away from current mouse position. i try with this code:
var body = d3.select("body");
var mouse = [];
var width = 1000;
var height = 600;
var numberOfTriangles = 30;
var isMouseMoving = false;
var triangle = d3.svg.symbolType["triangle-up"]
function drawTriangles(number) {
for (var i = 0; i < number; i++) {
var dim = Math.random() * 400;
svg.append("path")
.attr("d", triangle.size(dim))
.attr("transform", function(d) {
return "translate(" + Math.random() * width + "," + Math.random() * height + ")";
})
.attr("fill", "rgb(" + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + ")")
.attr("opacity", 2)
.attr("class", "path" + i);
}
}
function moveMouse() {
if (isMouseMoving) {
svg.selectAll('path').each(function(d, i) {
var self = d3.select(this);
self.attr('transform', function() {
return "translate(" + mouse[0] + "," + mouse[1] + ")";
})
})
}
}
var svg = body.append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
.on("mousemove", function() {
mouse = d3.mouse(this);
isMouseMoving = true;
});
drawTriangles(numberOfTriangles);
d3.timer(function() {
moveMouse()
});
but i have this error: "Uncaught TypeError: Cannot read property 'size' of undefined at drawTriangles".
Can someone help me? Thanks.
Your error is because of:
var triangle = d3.svg.symbolType["triangle-up"];
If you fix the typo on symbolTypes, this returns undefined. d3.svg.symbolTypes simply returns an array of available symbols, it is not a mechanism to create a new symbol path generator. That said, what you really wanted is:
var triangle = d3.svg.symbol().type("triangle-up");
This creates a proper triangle symbol generator.
Taking this a little further, I'm not sure what you mean by
that move away from current mouse position
Your code does the exact opposite and puts all the triangles on the mouse cursor...
EDITS
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 300,
height = 300;
var nodes = d3.range(200).map(function() { return {radius: Math.random() * 12 + 4}; }),
root = nodes[0],
color = d3.scale.category10();
root.radius = 0;
root.fixed = true;
var force = d3.layout.force()
.gravity(0.05)
.charge(function(d, i) { return i ? 0 : -1000; })
.nodes(nodes)
.size([width, height]);
force.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
.style("margin","20px");
var triangle = d3.svg.symbol().type("triangle-up");
svg.selectAll("path")
.data(nodes.slice(1))
.enter().append("path")
.attr("d", function(d) {
triangle.size(d.radius);
return triangle();
})
.style("fill", function(d, i) { return color(i % 3); });
force.on("tick", function(e) {
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) q.visit(collide(nodes[i]));
svg.selectAll("path")
.attr("transform", function(d){
return "translate(" + d.x + "," + d.y + ")";
});
});
svg.on("mousemove", function() {
var p1 = d3.mouse(this);
root.px = p1[0];
root.py = p1[1];
force.resume();
});
function collide(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
if (node.x > width) node.x = width;
if (node.x < 0) node.x = 0;
if (node.y > height) node.y = height;
if (node.y < 0) node.y = 0;
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
</script>

Specify startpoint to interpolate a circle on an arc when clicked on by the user

How do I provide the starting point of an arc for moving a circle along the path of the arc. I have a world map with several arcs displayed on it. I wish to interpolate the movement of a circle on the arc that has been selected by the user using the .on('click') event. I wish to know how I can identify the startPoint of the arc in question.
Specifically, I am not able to understand what parameters to provide in .attr("transform", "translate(" + startPoint + ")") attribute of the circle to enable the circle to start from the starting position of the arc.
At present, it passes the entire path and I receive the following error
d3.v3.min.js:1 Error: attribute transform: Expected number, "translate(M1051.5549785289…".
Although, surprisingly, the circle marker appears on the screen and interpolates along the first arc that has been drawn. However, I wish to change this interpolation to an arc that has been clicked by the user. In other words, how do I feed a new startPoint to the circle marker every time the user clicks on a different arc and to have a subsequent interpolation of the same.
var path3 = arcGroup.selectAll(".arc"),
startPoint = pathStartPoint(path3)
var marker = arcGroup.append("circle")
marker.attr("r", 7)
.attr("transform", "translate(" + startPoint + ")")
transition();
function pathStartPoint(path) {
var d = path.attr("d")
console.log(path)
dsplitted = d.split(" ");
return dsplitted[0].split(",");
}
function transition() {
marker.transition()
.duration(7500)
.attrTween("transform", translateAlong(path3.node()))
.each("end", transition);// infinite loop
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}
As #altocumulus states in the their comment, getPointAtLength doesn't need a starting point. It takes as an argument a distance from 0 to path length where 0 is the starting point. Here's a quick example, click on any path below:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var w = 400,
h = 400;
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
var data = []
for (var i = 0; i < 5; i++) {
data.push({
x0: Math.random() * w,
y0: Math.random() * h,
x1: Math.random() * w,
y1: Math.random() * h
});
}
var marker = svg.append("circle")
.attr("r", 20)
.style("fill", "steelblue")
.style("opacity", 0);
svg.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", function(d) {
var dx = d.x1 - d.x0,
dy = d.y1 - d.y0,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.x0 + "," + d.y0 + "A" + dr + "," + dr +
" 0 0,1 " + d.x1 + "," + d.y1;
})
.style("stroke", function(d, i) {
return d3.schemeCategory10[i];
})
.style("stroke-width", "10px")
.style("fill", "none")
.on("click", function(d){
marker
.style("opacity", 1)
.transition()
.duration(1000)
.attrTween("transform", translateAlong(this))
.on("end", function(d) {
marker.style("opacity", 0);
});
});
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")"; //Move marker
}
}
}
</script>
</body>
</html>

d3.js apply rect collision in force layout

There are a couple of questions regarding collision detection in force layout but with none I could solve the problem.
The closest is: rect collision detection d3js
In the code below I do have circles but I treat them as rectangles (as that is what I want to solve in the end - bounding box of circle plus label).
Here is the code:
var graph = {
"nodes":[
{"name":"name-1","rating":1,"id":2951},
{"name":"name-2","rating":2,"id":654654},
{"name":"3","rating":3,"id":6546544},
{"name":"4","rating":4,"id":68987978},
{"name":"5","rating":5,"id":9878933},
{"name":"6","rating":6,"id":6161},
{"name":"7","rating":7,"id":64654},
{"name":"8","rating":8,"id":354654},
{"name":"9","rating":9,"id":8494},
{"name":"10","rating":10,"id":6846874},
{"name":"11","rating":11,"id":5487},
{"name":"12","rating":12,"id":34},
{"name":"13","rating":13,"id":65465465},
{"name":"14","rating":14,"id":5443},
{"name":"15","rating":15,"id":313514},
{"name":"16","rating":16,"id":36543614},
{"name":"17","rating":17,"id":3434},
{"name":"18","rating":18,"id":97413},
{"name":"19","rating":19,"id":97414},
{"name":"27","rating":20,"id":9134371}
],
"links":[
{"source":0,"target":5,"value":6, "label":"publishedOn"},
{"source":2,"target":5,"value":6, "label":"publishedOn"},
{"source":7,"target":1,"value":4, "label":"containsKeyword"},
{"source":8,"target":10,"value":3, "label":"containsKeyword"},
{"source":7,"target":14,"value":4, "label":"publishedBy"},
{"source":8,"target":15,"value":6, "label":"publishedBy"},
{"source":9,"target":1,"value":6, "label":"depicts"},
{"source":10,"target":1,"value":6, "label":"depicts"},
{"source":7,"target":1,"value":6, "label":"manageWebsite"},
{"source":16,"target":2,"value":5, "label":"manageWebsite"},
{"source":5,"target":3,"value":6, "label":"manageWebsite"},
{"source":16,"target":4,"value":6, "label":"manageWebsite"},
{"source":12,"target":9,"value":2, "label":"postedOn"},
{"source":13,"target":1,"value":6, "label":"childOf"},
{"source":10,"target":8,"value":8, "label":"describes"},
{"source":8,"target":11,"value":6, "label":"containsKeyword"},
{"source":2,"target":5,"value":3, "label":"manageWebsite"}
]
}
var width = 900;
var height = 700;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-500)
.linkDistance(function(d) { return d.value * 10;})
.gravity(0.5)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var rect = svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all");
var container = svg.append("g");
force.nodes(graph.nodes).links(graph.links).start();
var links = container.append("g")
.attr("class", "links")
.selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.style("stroke-width", 2);
var nodes = container.append("g")
.attr("class", "nodes")
.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
var circle = nodes.append("circle")
.attr("r", function(d) { return d.weight * 2+ 12; })
.style("fill", function(d) {
return color(1/d.rating);
});
nodes.append("text")
.attr("x", function(d) { return d.weight * 2+ 12 + 4; })
.text(function(d) { return d.name });
force.on("tick", function() {
var xnodes = force.nodes();
var len = xnodes.length;
var q = d3.geom.quadtree(xnodes);
for (i = 0; i < len; i++) {
//does not work
//q.visit(collideNew2(xnodes[i]));
}
links.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
nodes.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
function collideNew2(node) {
var nx1, nx2, ny1, ny2, size;
size = 250;
nx1 = node.x;
nx2 = nx1 + size;
ny1 = node.y;
ny2 = ny1 + size;
return function(quad, x1, y1, x2, y2) {
var dx, dy;
if (quad.point && (quad.point !== node)) {
if (overlap(node, quad.point)) {
dx = Math.min(nx2 - quad.point.x, quad.point.x2 - nx1) / 2;
node.x -= dx;
quad.point.x -= dx;
dy = Math.min(ny2 - quad.point.y, quad.point.y2 - ny1) / 2;
node.y -= dy;
quad.point.y += dy;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
};
function overlap(n, p) {
var nx1, nx2, ny1, ny2, size;
size = 250;
nx1 = n.x;
nx2 = nx1 + size;
ny1 = n.y;
ny2 = ny1 + size;
var x1 = p.x;
var x2 = x1 + size;
var y1 = p.y;
var y2 = y1 + size;
if (((x1 > nx1 && x1 < nx2) || (x2 > nx1 && x2 < nx2)) && ((y1 > ny1 && y1 < ny2) || (y2 > ny1 && y2 < ny2))) {
return true;
}
return false;
}
It seems that the overlap functions as well as the selected "nodes" is wrong. How can I fix this?

D3 Force Layout with Image as Node but Wrong Images are Appended

I'm trying to create a force-layout with images as nodes that can be modified depending on user input (through a couple of checkboxes). However, when user is changing his/her input and the layout is redrawn, some images are showing up on top of the wrong node and I can't understand why.
The way I structured my code:
I pulled data from a csv
I created an "update" function that creates/modifies the data array depending user input
I created a "draw" function that draws the force layout every time the user changes his/her input (i.e. every time the function "update" is called)
The first part of my code seems to work fine. The array of data is created from the csv and dynamically updated correctly.
But when the graph is redrawn some images appear on the wrong node (even though the image showing up in the tooltip is the right one..).
Below is my "draw" function, I think that's where the problem is.
function draw(nodes) {
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.on("tick", tick)
.charge(-0.01)
.gravity(0)
.start();
function tick(e) {
// Set initial positions
nodes.forEach(function(d) {
d.radius = radius;
});
var node = svg.selectAll(".node")
.data(nodes);
var newNode = node.enter().append("g")
.attr("class", "node")
.on("click", function(d) {
div.transition().duration(300).style("opacity", 1);
div.html("<img src=img_med/" + d.name + ".png >")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY-10) + "px");
})
.on("mouseout", function (d) { div.transition().delay(1500).duration(300).style("opacity", 0);});
newNode.append("image")
.attr("xlink:href", function(d) { return "img_med/" + d.name + ".png"; })
.attr("x", -8)
.attr("y", -8)
.attr("width", 30)
.attr("height", 30)
.style("cursor", "pointer")
.call(force.drag);
node.exit().remove();
node.each(moveTowardDataPosition(e.alpha));
node.each(collide(e.alpha));
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
function moveTowardDataPosition(alpha) {
return function(d) {
d.x += (x(d[xVar]) - d.x) * 0.1 * alpha;
d.y += (y(d[yVar]) - d.y) * 0.1 * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + radius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
}
Any help/suggestions/pointers to help me understand why the wrong images show up on top of those nodes would be much appreciated!
Let me know if anything is unclear.
Thanks a lot!
Ok, I was just missing a key function to tell D3 how to match existing nodes and data.
I replaced
var node = svg.selectAll(".node")
.data(nodes);
by
var node = svg.selectAll(".node")
.data(nodes, function(d) {return d.name;});
and it worked.
Thanks Lars!

Categories