I believe I've followed the general update pattern on new React Props. D3 does the data calculation and rendering when new props are received, so that React does not have to render every tick.
D3 works well with a static layout. But when I receive new nodes and links at shouldComponentUpdate(nextProps) function, the nodes lack the following attributes:
index - the zero-based index of the node within the nodes array.
x - the x-coordinate of the current node position.
y - the y-coordinate of the current node position.
As a result, all new nodes have <g tranform=translate(undefined, undefined)/> and are clustered at the left top corner.
The way I update the props are by pushing new objects to the nodes array and links array. I don't understand why D3 doesn't assign d.x and d.y as it did for the initial setup at componentDidMount(). I have been struggling with this problem for days. Hope someone can help me out here.
Here is the ForceLayout.jsx:
//React for structure - D3 for data calculation - D3 for rendering
import React from 'react';
import * as d3 from 'd3';
export default class ForceLayout extends React.Component{
constructor(props){
super(props);
}
componentDidMount(){ //only find the ref graph after rendering
const nodes = this.props.nodes;
const links = this.props.links;
const width = this.props.width;
const height = this.props.height;
this.simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).distance(50))
.force("charge", d3.forceManyBody().strength(-120))
.force('center', d3.forceCenter(width / 2, height / 2));
this.graph = d3.select(this.refs.graph);
this.svg = d3.select('svg');
this.svg.call(d3.zoom().on(
"zoom", () => {
this.graph.attr("transform", d3.event.transform)
})
);
var node = this.graph.selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr("class", "node")
.call(enterNode);
var link = this.graph.selectAll('.link')
.data(links)
.enter()
.call(enterLink);
this.simulation.on('tick', () => {
this.graph.call(updateGraph);
});
}
shouldComponentUpdate(nextProps){
//only allow d3 to re-render if the nodes and links props are different
if(nextProps.nodes !== this.props.nodes || nextProps.links !== this.props.links){
console.log('should only appear when updating graph');
this.simulation.stop();
this.graph = d3.select(this.refs.graph);
var d3Nodes = this.graph.selectAll('.node')
.data(nextProps.nodes);
d3Nodes
.enter()
.append('g')
.attr("class", "node")
.call(enterNode);
d3Nodes.exit().remove(); //get nodes to be removed
// d3Nodes.call(updateNode);
var d3Links = this.graph.selectAll('.link')
.data(nextProps.links);
d3Links
.enter()
.call(enterLink);
d3Links.exit().remove();
// d3Links.call(updateLink);
const newNodes = nextProps.nodes.slice(); //originally Object.assign({}, nextProps.nodes)
const newLinks = nextProps.links.slice(); //originally Object.assign({}, nextProps.links)
this.simulation.nodes(newNodes);
this.simulation.force("link").links(newLinks);
this.simulation.alpha(1).restart();
this.simulation.on('tick', () => {
this.graph.call(updateGraph);
});
}
return false;
}
render(){
return(
<svg
width={this.props.width}
height={this.props.height}
style={this.props.style}>
<g ref='graph' />
</svg>
);
}
}
/** d3 functions to manipulate attributes **/
var enterNode = (selection) => {
selection.append('circle')
.attr('r', 10)
.style('fill', '#888888')
.style('stroke', '#fff')
.style('stroke-width', 1.5);
selection.append("text")
.attr("x", function(d){return 20}) //
.attr("dy", ".35em") // vertically centre text regardless of font size
.text(function(d) { return d.word });
};
var enterLink = (selection) => {
selection.append('line')
.attr("class", "link")
.style('stroke', '#999999')
.style('stroke-opacity', 0.6);
};
var updateNode = (selection) => {
selection.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")");
};
var updateLink = (selection) => {
selection.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
};
var updateGraph = (selection) => {
selection.selectAll('.node')
.call(updateNode);
selection.selectAll('.link')
.call(updateLink);
};
I've tried pushing new node into the node array at shouldComponentUpdate() function, instead of modifying the arrays in server. But the new node still appears on the left top corner with undefined position. So I am guessing my problem is at shouldComponentUpdate(). Any help is greatly appreciated!!
EDIT: found that Object.assign(...) does not return array. Changed it into array.slice() instead. Now all nodes are rendered with a position but not linked at all. Old nodes frayed from original positions as well.
Here is how it looks when new props go in and shouldComponentUpdate is triggered
I don't understand why the positions on the links don't correspond to the nodes.
the links in forceLink use by default object references to point to the source and target.
You don't show how you construct your links and nodes props, but you can get around it by calling id and setting an id accessor to point to your nodes logical id, so assuming your node have an id property this can be written like this:
.force("link", d3.forceLink(links).id(d => d.id).distance(50))
alternatively you could use the node's index as an accessor:
.force("link", d3.forceLink(links).id(d => d.index).distance(50))
or
.force("link", d3.forceLink(links).id((d, i) => i).distance(50))
-- Edit --
another measure that could help is to merge the properties of current nodes with the new nodes, this will let them retain position:
const updatePositions = (newNodes = [], currentNodes = []) => {
const positionMap = currentNodes.reduce((result, node) => {
result[node.id] = {
x: node.x,
y: node.y,
};
return result
}, {});
return newNodes.map(node => ({...node, ...positionMap[node.id]}))
}
then in your shouldComponentUpdate (note, this is not really where this code should live) you could call it like this:
var nodes = updatePositions(newProps.nodes, this.simulation.nodes())
and use nodes instead of newNodes.
Note that this code assumes nodes have a unique id property. Change this to fit your use case
You should also try adding a key function to your selections to identify your nodes and links, e.g.:
this.graph.selectAll('.node')
.data(nextProps.nodes, d => d.id) // again assuming id property
I solved my problem which I found was a combination of bugs:
Nodes had no links, or links don't correspond with nodes.
Firstly, my d3 graph and data had different ways to identify nodes - graph looking for index as links when my link data pointed at objects. I resolved this mismatch problem by changing both into id (i.e. looking for a string). This link suggested by #thedude pointed me to the right way. Resolving this problem led to the new nodes linked together correctly.
The old nodes were still separated, bounced further away, and the old links stay where they were first rendered.
I suspect this is caused by getting graph data from d3, which defined x, y, vx, vy and index properties. So what I did was getting rid of them when I receive currentGraph in server before I update the data.
removeD3Extras: function(currentGraph) {
currentGraph.nodes.forEach(function(d){
delete d.index;
delete d.x;
delete d.y;
delete d.vx;
delete d.vy;
});
currentGraph.links.forEach(function(d){
d.source = d.source.id;
d.target = d.target.id;
delete d.index;
});
return currentGraph;
}
That did the trick! Now it behaves how I intend to with no errors on console however I zoom, click and drag.
BUT there is room for improvement:
the links are on top of nodes
sometimes nodes are on top of each other during tick, thus requiring dragging and dropping.
Related
I am updating a D3 graph using an async method to call a function called "node_graph.addNode(mesg);".
One of the values of the mesg json is called "level", and addNode does is arrange the nodes higher or lower according the value of that value, as shown here:
There is an array of urls pointing to the different png images.
I would like to have the PNG to change as well.
This works fine, when level changes, the nodes rearrange as expected.
However, the PNG doesn't change to reflect the new level. A page refresh works of course.
Where am I going wrong?
I've read somewhere that the correct way is to use inline svg images?
(My knowledge of D3 is a bit sketchy.)
Pertinent code follows (edited for brevity):
let levelImage = {
'1': '/static/img/state_running.png',
'3': '/static/img/state_shutoff.png',
'5': '/static/img/state_paused.png',
};
// snip
this.addNode = function (msg) {
let uuid = msg['uuid'];
let name = msg['name'];
let level = msg['level'];
let img = levelImage[level];
containBox=container.node().getBoundingClientRect();
/* initial starting position for nodes */
let x = Math.random()*containBox.width;
let y = Math.random()*containBox.height;
let index = findNodeIndex(uuid);
if (index == -1) {
nodes.push({"id": uuid, "uuid": uuid, "name":name, "img":img, "level": level});
update();
}
else {
nodes[index].uuid=uuid;
nodes[index].level = level;
nodes[index].img = levelImage[level]; // this gets the correct image given the level value
update();
}
this.start();
}
let update = function () {
node = vis.select(".nodes").selectAll("g.node")
.data(nodes);
let nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
)
.on('mouseover' , mouseOver)
.on('mouseout' , mouseOut);
nodeEnter.append("defs")
.attr("id", function(d){ return 'p-'+d.id;})
.append("pattern")
.attr("id", function(d){ return 'i-'+d.id;})
.attr("height", 1)
.attr("width", 1)
.append("image")
.attr("height", 100)
.attr("width", 100)
.attr("xlink:href", function(d){return d.img;});
Thoughts appreciated - Thanks
I am trying to write a transitioning bar graph that uses two CVS files. I know that both of the files are loading properly because it shows in the console that the first one loads with the page and the second one loads when you click the update button.
The only thing that I have really thought of trying was changing the svg select to group instead of selecting all rectangles incase there was something screwed up there.
This block is creating the svg element, bringing in the first CSV file, and appending the rectangles onto the chart. My only thought for what the problem could be is that it is inside a function, but if I take it out of the function how do I bind the data to them?
//Creating SVG Element
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3.select('#chart')
.append('svg')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand().range([chart_pad_x, chart_w -
chart_pad_x]).padding(0.2);
var y_scale = d3.scaleLinear().range([chart_pad_y, chart_h -
chart_pad_y]);
//Data-------------------------------------------------------------------
d3.csv('data.csv').then(function(data){
console.log(data);
generate(data); });
function generate(data){
//Scale domains
x_scale.domain(d3.extent(data, function(d){ return d }));
y_scale.domain([0, d3.max(data, function(d){ return d })]);
//Create Bars
svg.select('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i){
return x_scale(i);
})
.attr('y', function(d){
return y_scale(d);
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d){
return y_scale(d);
})
.attr('transform',
"translate(0,0)")
.attr('fill', '#03658C')
'''
The results I have experienced is a blank window with just the update button. As previously stated I know that the data is being generated because I can see it in the console.
Try using the following:
svg.selectAll('rect')
.data(data)
If you use svg.select this will only make the data binding with the first element found.
d3.select(selector): Selects the first element that matches the specified selector string. If no elements match the selector, returns an empty selection. If multiple elements match the selector, only the first matching element (in document order) will be selected. For example, to select the first anchor element:
This should be clear if you inspect the DOM nodes.
To fix the issue lets change some things in your code:
Lets create a dummy fetch function:
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
You are also using a scaleBand with an incomplete domain by using the extent function:
d3.extent(): Returns the minimum and maximum value in the given iterable using natural order. If the iterable contains no comparable values, returns [undefined, undefined]. An optional accessor function may be specified, which is equivalent to calling Array.from before computing the extent.
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
console.log(x_scale.domain()) // [min, max]
The scaleBand needs the whole domain to be mapped
Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
If we continue using that scale we will be only to get two values for our x scale. Lets fix that with the correct domain:
x_scale.domain(data);
Lastly use the selectAll to create the data bind:
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
This should do the trick.
Complete code:
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3
.select('#chart')
.append('svg')
.style('background', '#f9f9f9')
.style('border', '1px solid #cacaca')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand()
.range([chart_pad_x, chart_w - chart_pad_x])
.padding(0.2);
var y_scale = d3.scaleLinear()
.range([chart_pad_y, chart_h - chart_pad_y]);
//Data-------------------------------------------------------------------
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
function generate(data) {
console.log(d3.extent(data, function(d) { return d }));
//Scale domains
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
// Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
x_scale.domain(data);
y_scale.domain([0, d3.max(data, function(d) {
return d
})]);
//Create Bars
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
}
Working jsfiddle here
I have a scatter plot and a table. Each circle in the scatter plot has a corresponding row in the table. When I apply classes to the circles for CSS purposes, I also want to have that same class be assigned to the corresponding table row. They have the same data value, but are appended to separate elements.
Here is my circle class event:
my_circles.each(function(d,i) {
if (my_bool===true) {
d3.select(this).classed('selected',true);
//d3.selectAll('tr').filter(d===???)
}
});
I was trying to use a filter to select only the table rows of matching d value, but it didn't quite work out, I didn't know how to finish the line. Which got me thinking, maybe there is a better way, like the post title, assign classes to all elements bound to the same data.
If you have another solution aside from any of my ideas, that would be fine too.
Probably the easiest solution will be to check in the .classed() method for the tr selection, if the data bound to that tr matches the one for the selected circle.
my_circles.each(function(d,i) {
if (my_bool===true) {
d3.select(this).classed("selected",true);
d3.selectAll('tr')
.classed("selected", trData => d === trData); // Set class if data matches
}
});
This, however, is a bit clumsy and may be time-consuming because it will iterate over all trs each time this code is called. In case this is in an outer loop for handling multiple selected circles—as mentioned in your comment—things will get even worse.
D3 v4
For a slim approach I would prefer using D3's local variables, which are new to v4, to store the references between circles and table rows. This will require just a one-time setup which will depend on the rest of your code, but might go somewhat along the following lines:
// One-time setup
var tableRows = d3.local();
my_circles.each(function(d) {
var row = d3.selectAll("tr").filter(trData => d === trData);
tableRows.set(this, row); // Store row reference for this circle
});
This creates a new local variable tableRows which is used to store the reference to the corresponding table row for each circle. Later on you are then able to retrieve the reference to the row without the need for further iterations.
my_circles.each(function(d,i) {
if (my_bool===true) {
d3.select(this).classed('selected',true);
tableRows.get(this).classed("selected", true); // Use local variable to get row
}
});
D3 v3
If you are not yet using D3 there are, of course, other ways to achieve the same thing. Personally, I would prefer using a WeakMap to store the references. Because the API of the WeakMap also features get and set methods similar to d3.local, all you need to do is to change the line creating the local reference store while keeping the rest of the above code as is:
// var tableRows = d3.local();
var tableRows = new WeakMap(); // use a WeakMap to hold the references
You can use dataIndex for this purpose. Here is a code snippet for the same.
var data = ["A", "B", "C"];
var color = d3.scale.category10();
var container = d3.select("body")
.append("svg")
.attr("height", 500)
.attr("width", 500);
var my_circles = container.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("name", function(d, i) {
return "circle" + i
})
.attr("r", 10)
.attr("cx", function(d, i) {
return (i + 1) * 50
})
.attr("cy", function(d, i) {
return (i + 1) * 50
})
.style("fill", function(d, i) {
return color(i)
});
container.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("name", function(d, i) {
return "rect" + i
})
.attr("width", 15)
.attr("height", 15)
.attr("x", function(d, i) {
return i * 50 + 200
})
.attr("y", function(d, i) {
return (i + 1) * 50
})
.style("fill", function(d, i) {
return color(i)
});
my_circles.each(function(d, i) {
d3.select(this).classed("selected" + i, true);
container.selectAll("[name=rect" + i + "]").classed("selected" + i, true);
});
svg {
border: 1px solid black;
background: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I am new to D3JS and you can find the code at http://jsfiddle.net/3n8wD/
I am a facing an issue and any pointers would help
When I move the line it separates as expected, but as soon as I try to move the circles away it jumps back to the line.
On inspecting the Array, looks like the circle array is updating as I am moving the link, not sure what is causing that.
Any help on this would be highly appreciated. below is the code that i have on jsfiddle
var width = 960,
height = 500;
graph1 = {"nodes":[{"x":444,"y":275},{"x":378,"y":324}],"links":[{"source":1,"target":0}]}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var onDraggable = function(d1) {
console.log(d1);
d1.x = d3.event.x, d1.y = d3.event.y;
d3.select(this).attr("cx", d1.x).attr("cy", d1.y);
//console.log(d1);
link.filter(function(l) { return l.source === d1; }).attr("x1", d3.event.x).attr("y1", d3.event.y);
link.filter(function(l) { return l.target === d1; }).attr("x2", d3.event.x).attr("y2", d3.event.y);
}
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("drag", onDraggable)
var onDraggable1 = function(d) {
//d.x1 = d3.event.x1, d.y1 = d3.event.y1, d.x2=d3.event.x2, y2=d3.event.y2;
var mouseX = d3.mouse(this)[0];
var mouseY = d3.mouse(this)[1];
var relativeX = mouseX-d.source.x;
var relativeY = mouseY-d.source.y;
console.log(d);
//console.log(d);
// d3.select(this).attr("x1", d3.event.x).attr("y1", d3.event.y).attr("x2", d3.event.x).attr("y2", d3.event.y);
d.source.x= d.source.x+relativeX;
d.source.y = d.source.y+relativeY;
d.target.x= d.target.x+relativeX;
d.target.y = d.target.y+relativeY;
d3.select(this).attr("x1", d.source.x).attr("y1", d.source.y);
d3.select(this).attr("x2", d.target.x).attr("y2", d.target.y);
}
var drag1 = d3.behavior.drag()
.origin(function(d) { return d; })
.on("drag", onDraggable1);
graph1.links.forEach(function(d) {
d.source = graph1.nodes[d.source];
d.target = graph1.nodes[d.target];
});
var node = svg.append("g").attr("class","node")
.selectAll("circle")
.data(graph1.nodes)
.enter().append("circle")
.attr("r", 4)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.call(drag);
var link = svg.append("g").attr("class","link")
.selectAll("line")
.data(graph1.links)
.enter().append("line")
.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; })
.attr("stroke-width", 2)
.attr("stroke", "black")
.call(drag1);
The reason that your nodes are "jumping" to the end of the line, even after the line has been moved away, is because the data object for the node is the same object as the source/target data object for the line. The d value for the node and the d.source or d.target value for the link are just pointers (references) to the same object in the browser's memory. Everything you do to that data object is reflected in both the link and the node. That's what makes the node-drag function work: you change the data object directly, and then you update the position of the line and the circle to match their already-changed data value.
So, even though you don't update the circle's cx and cy position at the time you move the line, the statements d.source.x = etc. in the line drag method are setting the d.x and d.y values for the nodes. Then in the node drag method, when you access these values to determine the new position, the movement is determined relative to the end of the line, not to the position of the circle on screen.
So how do you get the behaviour you want, to separate the line from the node? You need to create a new data object for the line's source and target when you start to drag it, one that no longer references the nodes' data objects. You only need to do this once, at the start of the drag, so you use the dragstart event of the drag behaviour object:
var drag1 = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", separateNodes);
.on("drag", onDraggable1);
The separateNodes method is going to be quite short. All we have to do is, for both the target and the source of the link, create a new data object that is a copy of the existing target/source object (but which can be edited independently of the original object). Javascript doesn't have any default methods for copying an object (although various extensions do), but if the object in question just consists of x and y attributes, it is easy to do.
var separateNodes = function(d) {
//create new data objects that are copies of d.source and d.target
var newSource = {x:d.source.x, y:d.source.y};
var newTarget = {x:d.target.x, y:d.target.y};
//set the new objects as the target/source of this link
d.source = newSource;
d.target = newTarget;
}
You could even skip the variable declarations and just merge it all into one line for source and one for target, but I find it easier to understand like this. I should mention, however, that this approach works because x and y are both just simple data types (numbers), not pointers to objects. So when it says x:d.source.x, you are actually getting a new copy of that number which you can change without having it affect the original object that contained that number.
If, however, your nodes and links also have a value property that is a complex data object, then you have to decide what you want to do when the line separates from the node: do you want to create a copy of the entire value data object, or maintain the link to the original object, or replace it with a null (empty/undefined) object? The first case is complicated, and depends on the data structure of the value object; for the second you don't need to do anything (except remember that you've got multiple pointers to the same object); and for the last case you just add a value:null, attribute to the newSource and newTarget object definitions in your separateNodes function.
Hope that all makes sense,
--ABR
I'm having a bit of trouble getting a something to work with D3.js. Namely, I'm trying to make a tree of nodes, using the basic code from http://bl.ocks.org/mbostock/1021953.
I switched it to load the data inline, as opposed to loading from file, because I'm using it with a Rails application and don't want to have repetitive information. I switched the line so that you could see the format of my data.
Anyways, here's the bulk of my code:
<%= javascript_tag do %>
var nodes = [{"title":"Duncan's Death","id":"265"},{"title":"Nature Thrown Off","id":"266"},{"title":"Cows Dead","id":"267"},{"title":"Weather Bad","id":"268"},{"title":"Lighting kills man","id":"269"},{"title":"Macbeth's Rise","id":"270"}];
var links = [{"source":"265","target":"266","weight":"1"},{"source":"266","target":"267","weight":"1"},{"source":"266","target":"268","weight":"1"},{"source":"268","target":"269","weight":"1"}];
var firstelement = +links[0].source;
links.forEach(function(l) {
l.source = +l.source;
l.source = l.source-firstelement;
l.target = +l.target
l.target = l.target-firstelement;
});
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-1000)
.linkDistance(300)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
force
.nodes(nodes)
.links(links)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.weight); });
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "circle_node")
.attr("r", 50)
.style("fill", function(d) { return color(d.id); })
node.append("title")
.text(function(d) { return d.title; });
node.append("text")
.attr("x", function(d) { return d.x; } )
.attr("y", function(d) { return d.y; })
.text(function(d) { return d.title; });
force.on("tick", function() {
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; });
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
});
<% end %>
This seems like it should work to me, but I can seem to manage it. The links work, but the nodes all remain in the top left corner. I've tried just entering the circles directly and appending the text to them (staying close to the source code I listed above,) but while the circles behave properly, it doesn't display the text. I'd like the title to be centered in the nodes.
More generally, I'm kind of confused by how this is working. What does "d" refer to within lines like
function(d) { return d.source.x; }
It seems to be declaring a function and calling it simultaneously. I know that it doesn't have to be specifically the character "d," (for instance, switching the "d" to an "a" seems to make no difference as long as it's done both in the declaration and within the function.) But what is it referring to? The data entered into the object that's being modified? For instance, if I wanted to print that out, (outside of the attribute,) how would I do it?
Sorry, I'm new to D3 (and fairly new to JavaScript in general,) so I have a feeling the answer is obvious, but I've been looking it up and through tutorials and I'm still lost. Thanks in advance.
First, there's a simple problem with your code that is causing all your nodes to stay in the top left corner. You are trying to position each node using the code:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
However, node is a selection of gs which do not take x and y attributes. Instead, you can move each node using translate transform, e.g.
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
Making this change should allow the nodes to move around.
Next, moving to your question about "d", I think the first thing you need to understand is what you can do with a selection of elements in D3. From the docs: a selection (such as nodes) "is an array of elements pulled from the current document." Once you have a selection of elements, you can apply operators to change the attributes or style of the elements. You can also bind data to each element.
In your case, you are binding data to a selection of gs (nodes):
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
You are then using attr to change the position of each node. However, instead of setting the x and y attributes of each element to the same value, you are passing attr an anonymous function that will return a (presumably different) position for each node:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
This behavior is also explained in the docs for attr:
Attribute values and such are specified as either constants or
functions; the latter are evaluated for each element.
Thus, d represents an individual element (Object) in nodes.
So going back to your code, on each tick two things are happening:
The position of each node (data) is being recalculated by force.
Each corresponding element is then being moved to its new location by the anonymous function you pass to force.on.