I have a force layout with nodes defined as follows...
nodes = someData.map(function (d) {
return {
image_location: d.image_location,
image_width: d.image_width,
image_height: d.image_height,
aspect_ratio: d.aspect_ratio,
radius: circleRadiusScale(d.someCount),
x: width/2,
y: height / 2,
px: width/2,
py: height/2,
cx: width/2,
cy: height / 2
};
});
And then the force layout being created later in the code with...
force = d3.layout.force()
.gravity(.05)
.alpha(0.1)
.size([width, height])
.on("tick", tick);
force.nodes(nodes)
.start();
The reason I've forced the x/y, px/py, cx/cy values is that I'm simply trying to ensure that the nodes don't always start being projected at the top left of the browser (which is only for a split second until the force simulation takes affect, but is pretty noticeable especially on firefox or mobile devices).
What's strange to me is that I'm starting the force layout with my x/y, cx/cy, px/py values BEFORE the code I've written joining circles, images etc that the browser should display - in other words, this code comes AFTER defining/starting the force layout...
node = svg.selectAll(".node")
.data(force.nodes(), function(d) { return d.id; })
.enter()
.append("g")
.attr("class", "node") etc to append circles, images...
So I'm wondering how to hold back the projection of the nodes by the browser until I know the force layout has an initial position of something other than 0,0. Thanks for any thoughts!
I would check the position inside the tick handler function and initialise once the condition has been met:
var initialized = false;
force.on("tick", function() {
if(!initialized) {
// can also check multiple nodes here...
if(nodes[0].x != width/2 && nodes[0].y != height/2) initialize();
} else {
// update node positions
}
});
function initialize() {
node = svg.selectAll(".node")
.data(force.nodes(), function(d) { return d.id; })
.enter()
.append("g")
.attr("class", "node") // etc
initialized = true;
}
In my case, nodes inited and displayed before call tick(). So they will display at top left of screen firstly, after that call tick() to translate to real position in graph.
Just hidden node when add graphic:
nodeEnter.style("visibility", "hidden")
And, visible in tick() function:
nodeEnter.style("visibility", "visible")
Related
I try to port to d4.v4.js some graph drawing built with version 3.
I quite don't understand how to attach svg nodes and links to the simulation with atlas force.
The simplified version using d3.v3 is shared on fiddle, note that all nodes are grouped in a <g> element, and each node, simplified as a circle in this example is inserted in a <g> element as well. Similarly, links are grouped in a <g> element. I need those to draw more complexe networks.
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
var force = d3.layout.force()
.gravity(.15)
.distance(100)
.charge(-1200)
.friction(0.8)
.size([width, height]);
// push initial sets of nodes and links
var nodes = force.nodes(), links = force.links();
Array.prototype.push.apply(nodes, data.nodes);
Array.prototype.push.apply(links, data.edges);
My reference for drawing a network with d3.v4 in shared on fiddle as well, it actually works, however each node is not in a <g> element as required (I guess) for my more complex graphs.
This last fiddle shows the d3.v4 version of the simplified graph, as one can see, nodes remain in the upper left corner, only zoom works.
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink()
.id(function(d) { return d.id; })
.distance(function(d) { return (1-d.value*d.value*d.value)*20; }))
.force("charge", d3.forceManyBody().strength(-200))
.force("center", d3.forceCenter(width / 2, height / 2));
var pane = svg.append("g");
var link = pane.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return d.value*d.value*d.value*10; });
var node = pane.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) { return d.value*5 })
.style("fill", function(d) { return color(d.group); })
My question is how should I attach nodes (<g>) and links to "simulation"? What is wrong in my script?
Many thanks in advance for your help.
Let's take your last fiddle with the d3v4 implementation.
2 changes and it will work.
1) the ticked function to move the elements. Since nodes are svg groups now, you can't use cx and cy attributes (good for circles). You need to use transform translate like this:
node
.attr("transform", function(d) {
return "translate(" + d.x + ", " + d.y + ")";
});
2) The node variable there contains only the update selection, since you called the enter selection nodeEnter. And since all elements are in the enter selection (just created), you don't move anything.
So you just need to merge the two selections like this:
nodeEnter.append("circle")
.attr("r", function(d) { return d.value*10 })
node.exit().remove();
node = nodeEnter.merge(node);
You can read more details in Mike Bostock documentation about this. Check also this and other similar bl.ocks.
To correctly target the links in ticked you should also merge enter and update link selections in the same way.
As a side note, you don't need to link source and target objects in the links as you do in getGraph function, since this is done by d3 simulation already. So you could just use your json object as data.
Here is a working fiddle.
Hope it is clearer how to upgrade d3 v3 force directed layout to d3 v4 force simulation :D
I have got this d3.js force layout where the nodes are rectangles with associated text. Whenever a node is dragged the position is fixed, like in mike's example.
I want the nodes to snap to an invisible grid when they are dragged. So that they are neatly aligned. Therefore I've added this to the drag event:
var grid = 50;
d3.select(this)
.attr("x", function(d) { return Math.round(d3.event.x/grid)*grid; })
.attr("y", function(d) { return Math.round(d3.event.y/grid)*grid; });
Somehow this is not working as expected. I guess it has something to do with the tick function of the force layout. But I'm not sure, hence the force.stop(), force.start() calls to see whether that helped.
I've also tried to nest the rect and text in a g element and use transform(translate) to position the nodes. But also without succes.
https://jsfiddle.net/e5fxojvm/
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
function dragstarted(d) {
d3.select(this)
.classed("fixed", d.fixed = true);
}
function dragged(d,i) {
//force.stop();
var grid = 50;
var nx = Math.round(d3.event.x/grid)*grid;
var ny = Math.round(d3.event.y/grid)*grid;
d.px = nx;
d.py = ny;
}
function dragended(d) {
//force.start();
console.log(this);
}
the coordinates for nodes are taken from d.x and d.y for each data point in the nodes array, so though you update the on-screen element's x and y attributes, the next tick resets it to d.x and d.y, giving that little movement you see in your demo (though it doesn't move again after that point as the node is fixed)
You need to discretise the node position data - d.x and d.y - not just the node DOM element's position attributes, and unintuitively enough for fixed nodes this seems to be done through d.px and d.py - the previous node coordinates. It seems fixed nodes in the tick calculate d.x and d.y by setting them to d.px and d.py (https://github.com/mbostock/d3/blob/master/d3.js#L6299)?
I am working on developing a force directed graph in D3. Right now when I mouseover nodes it changes the opacity of the connected links from 0 to 1 and then when I mouseout it returns the opacity back to 0.
That works fine but what I am having trouble with is making a click on the node maintain the opacity of the links at 1 even after the mouseout event. Then I want to be able to click on other nodes to make their links opacity 1 as well. Then also be able to click on the some of the previously clicked nodes to be able to return the opacity of their associated links to 0.
In short, I want to be able to toggle the opacity of the associated links of a node with out it being affect by mouseout events. A sample of my current code is below. I am thinking I might have to set a new id to toggle on and off when I click on a node?
var nodeClick = function(d) {
svg.selectAll(".link")
.filter(function(p) {
return _(d.facets).contains(p.target.name)
})
.transition()
.style('stroke-opacity', 0.9);
};
var overText1 = function(d) {
svg.selectAll(".link")
.filter(function(p) {
return _(d.facets).contains(p.target.name)
})
.transition()
.style('stroke-opacity', 0.9);
};
var overText0 = function(d) {
svg.selectAll(".link")
.transition()
.duration(500)
.style('stroke-opacity', 0);
};
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", function (d) {
return d.group === 1 ? "nodeBig" : "node";
})
.attr("r", function(d) {return d.radius })
.style("fill", function (d) {
return color(d.group);
})
.on("mouseover", overText1)
.on('click', nodeClick)
.on('mouseout', overText0)
.call(force.drag);
I actually finally figured this out on my own. I created a lock field that accepts either a "true" or "false". Then I put an if statement in the mouseoff function that only enables mouseoff functionality on elements that don't have "true" in their lock field.
I'm a newby in D3.js. I adapted this example from Mike Bostock, into this fiddle, which provides an example of Point-Along-Path Interpolation based on attrTween and getPointAtLength.
In the original example a single circle shape is introduced, which follows a simple path. In the adapted example, instead of introducing a single circle, a bunch of circles are generated which follow a more complex shape, a toy example path made in Inkscape.
The animation runs fine for a number of iterations, but after a short while the circles appear to get caught up in a loop and ultimately the page freezes up. However, if only a single circle is generated e.g. var RadiusData = [20]; (see code below), the animation keeps looping just fine.
What could be causing this? Is there an easy way to avoid this behavior?
var w = $(window).width(),
h = $(window).height();
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + 0.25*w + "," + 0 + ")");
//some toy path data
var dInkScape ="m 360.1639,630.31678 c 1.0609,13.05167 -195.29107,-273.68628 -203.49722,-275.81173 -22.23818,-5.75983 -24.83733,-34.59299 -15.23811,-51.66666 17.17076,-30.54078 59.06286,-32.72422 85.71428,-14.04764 39.11203,27.40863 40.85844,83.86959 12.85717,119.7619 C 202.67874,456.39146 131.20349,457.65152 86.190506,420.21936 29.546262,373.1148 28.796105,286.43841 75.714265,232.36222 132.53844,166.8687 234.51201,166.64035 297.61902,223.07645 c 74.36943,66.50798 74.06939,183.83474 8.09531,255.95237 C 229.54464,562.29148 96.8291,561.45911 15.714334,485.93366 -76.453418,400.11684 -75.086213,251.98848 9.9999617,161.88605 105.45379,60.804734 269.012,62.70845 368.09519,157.36214 478.09632,262.44568 489.74023,530.06221 385.51394,638.12097 z";
var path = svg.append("svg:path")
.attr("d", dInkScape);
//some random data for the circle radii
var RadiusData = [20,50,25,5,40,22,50,66,72,23];
//introduce a circle for each element, set radius and give it some random color
var circle = svg.selectAll("circle")
.data(RadiusData).enter()
.append("svg:circle")
.attr("r", function(d){return d;})
.style("fill",function(d,i) {return "hsl(" + 120 + 100 *Math.random() + ",100%,25%)";})
.attr("transform", "translate(0," + -h / 3 + ")");
//with a 1 second delay introduce a new circle
function transition() {
circle.transition()
.duration(5000)
.delay(function(d,i){return 1000*i;})
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
}
transition();
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
The problem is that you are re-calling the transition function at the end of each circle's movement, but that function creates a new transition for every circle:
//with a 1 second delay introduce a new circle
function transition() {
circle.transition() //add a transition to every circle
.duration(5000)
.delay(function(d,i){return 1000*i;})
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
//re-run this function after *each* circle's transition completes
}
One option for fixing it is to have the each function only re-start the transition for the first element, i.e. when i==0 or !i is true:
function transition() {
circle.transition()
.duration(5000)
.delay(function(d,i){return 1000*i;})
.attrTween("transform", translateAlong(path.node()))
.each("end", function(d,i){if (!i) transition(); });
}
http://jsfiddle.net/A88W3/8/
The other option, as #mbostock just suggested, is to make your function only apply to a single element:
function transitionThis(d,i) { //index is given to the function
d3.select(this).transition()
.duration(5000)
.delay(1000*i) //not a function anymore
.attrTween("transform", translateAlong(path.node()))
.each("end", transitionThis); //repeat for this element
}
circle.each(transitionThis); //start transitions for each
http://jsfiddle.net/A88W3/9/
Or, if you only wanted to apply the delay once, to stagger the start times, but then to have all the circles move evenly without stopping at the beginning of the path:
function transitionThis(d,i) {
d3.select(this).transition()
.duration(5000) //no delay once started
.ease("linear") //move steadily at all points on path
.attrTween("transform", translateAlong(path.node()))
.each("end", transitionThis); //repeat for this element
}
circle.transition().duration(0)
.delay(function(d,i){return 1000*i;}) //stagger starts
.each(transitionThis); //start transitions for each
http://jsfiddle.net/A88W3/10/
One other thing: at least during debugging, it's always a good idea to code-in a way to stop any infinite loops. I did so in the above fiddles by adding a click function to the svg as a whole that creates a new transition on the circles, interrupting the infinite looping versions:
svg.on("click", function() { //Stop the infinite transitions!
circle.transition(); //create a new empty transition
});
The problem is you’re starting a new transition on all on the circles whenever any one of the circles finishes a transition, leading to an explosion of overlapping transitions if your initial selection has more than one element.
The transition.each callback is called for each element in the selection. You probably want to say d3.select(this) to create a transition for an individual element, as in the chained transitions example.
I am trying to create an interactive org chart such that when I click on a box that box is repositioned in the centre of the SVG container and all other elements transition as well but remain in the same relative position. So if you click the top box in the list, they all move down together. Then if you click one of the lower boxes they all move up together but always so the selected box is in the centre. If you click on a box which is already in the middle it should not move but at the moment they are flying all over the place.
I have got this working for the first click but on each subsequent click the boxes start flying all over the place. I am using the mouse listener to get the current position and calculate an offset to centre the selected box that I feed into transform/translate. I think this is where the strange behaviour is coming from because the offset is calculating correctly (viewed through console.log) but the applied transition is not equal to this calculation.
I have read many posts about transform/translate but they all seem to apply to a single transition, not multiple sequential transitions. I have tried using .attr(transform, null) before each new transition but this didn't work. I have also tried to dynamically extract the current x,y of the selected component and then update these attributes with the offset value but this didn't work either. Am really stuck with this and any help is greatly appreciated!
Thanks,
SD
<script type="text/javascript">
var cwidth = 1000;
var cheight = 500;
var bwidth = 100;
var bheight = 50;
// container definition
var svgContainer = d3.select("body").append("svg")
.attr("width",cwidth)
.attr("height",cheight)
.on("mousemove", mousemove);
// Background gray rectangle
svgContainer.append("svg:rect")
.attr("x",0)
.attr("y",0)
.attr("width",cwidth)
.attr("height",cheight)
.style("fill", "lightgrey");
// data
var secondData = [
{ "idx": 1, "name": "Commercial" },
{ "idx": 2, "name": "Finance" },
{ "idx": 3, "name": "Operations" },
{ "idx": 4, "name": "Business Services" }
];
var secondElements = secondData.length;
// group definition
var secondNodes = svgContainer.append("g")
.attr("class", "nodes")
.selectAll("rect")
.data(secondData)
.enter()
.append("g")
.attr("transform", function(d, i) {
d.x = 300;
d.y = ((cheight/secondElements)*d.idx)-bheight;
return "translate(" + d.x + "," + d.y + ")";
});
// Add elements to the previously added g element.
secondNodes.append("rect")
.attr("class", "node")
.attr("height", bheight)
.attr("width", bwidth)
.style("stroke", "gray")
.style("fill", "white")
.attr("y", function() {return -(bheight/2);})
.on("mouseover", function(){d3.select(this).style("fill", "aliceblue");})
.on("mouseout", function(){d3.select(this).style("fill", "white");})
.on("mousedown", center);
// Add a text element to the previously added g element.
secondNodes.append("text")
.attr("text-anchor", "left")
.attr("x", 15)
.attr("y",5)
.text(function(d) {return d.name;});
// gets current coordinates for transition
var current = [0,0];
var xshift = 0;
var yshift = 0;
// get offset to centre from current mouse location
function mousemove() {
//console.log(d3.mouse(this));
current = d3.mouse(this);
xshift = 500 - current[0];
yshift = 250 - current[1];
}
//applies transitions
function center(d) {
secondNodes.selectAll("rect")
.transition()
.delay(0)
.duration(500)
.attr("transform", "translate(" + xshift + "," + yshift + ")")
.each("end", function() {
secondNodes.selectAll("text")
.transition()
.delay(0)
.duration(0)
.attr("transform", null);
});
}
</script>
If you want everything to keep its relative position, it seems to me that something far easier to do would be to include everything in a g element that you can set the transform attribute one. That is, instead of moving many elements, you would have to move just the top-level container. The code you have for handling clicks would remain pretty much the same except that you only need to set the transform attribute on that one element.