Dynamically create nodes in D3.js - javascript

Note: My first StackOverFlow post, please bear with me.
Aim of my code:
Take a connected graph as an input in a user friendly way ( I mean that user should be able to add a node on say, a double click. And when the user wants to connect two nodes, he/she may just select two nodes and there should be a way for the user to input distance between the two selected nodes).
After successfully taking the graph as input, I want to perform various operations on the graph like telling what is the shortest path from node ‘A’ to node ‘B’, for example.
Approach:
I have decided to use D3.js for the purpose. I am a newbie in JavaScript/jQuery but still I learnt D3.js from https://www.dashingd3js.com/table-of-contents .
I have seen people create amazingly beautiful projects involving graph using D3.js but I am kind of struggling even in the basics.
Attempt:
I wrote the following code to connect two nodes:
<!DOCTYPE html>
<html>
<head>
<title>D3 Try</title>
<script type="text/javascript" src="d3.min.js"></script>
</head>
<style>
.node {
fill: #ccc;
stroke: #fff;
stroke-width: 2px;
}
.link {
stroke: #777;
stroke-width: 2px;
}
</style>
<body style="background-color: red">
<h2>Hello world</h2>
<script type="text/javascript">
var height=500,width=960;
// If we leave out the x and y coordinates of the visualization, d3 selects
// random positions for the nodes
// nodes are arbitrary objects
var nodes = [ {x: width/3, y: height/2 } , {x: 2*width/3, y: height/2} ];
// Define the array which contains information about the links
var links= [ {source: 0, target: 1}];
// container to hold the visualisation
var svg=d3.select('body').append('svg').attr('width',width).attr('height',height);
// Create force layout object which contains dimensions of the container and
// the arrays of nodes and links
var force=d3.layout.force().size([width,height]).nodes(nodes).links(links);
// Documentation says that define a function in place of width/2 but fir bhi , how?
force.linkDistance(width/2);
var link=svg.selectAll('.link').data(links).enter().append('line').attr('class','link');
var node=svg.selectAll('.node').data(nodes).enter().append('circle').attr('class','node');
force.on('end',function(){
node.attr('r', width/25)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return 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; });
});
force.start();
</script>
</body>
</html>
Problems I faced and what I really want to ask:
The above code connects two nodes, okay great, but how do I dynamically create such nodes on user's double click ? It may here be noted that not only a node has to be drawn on SVG, but the nodes and links arrays are to be updated too. How do I update those hard-coded arrays dynamically so that they store the most recent information in them?
The distance between the two nodes must be entered by the user, here it is a constant width/2 . I checked out the API on github, they say that define a function instead of a constant.I searched but couldn't find any example of that. Even if I use the D3 supplied variable d, its of no use since it has to be user dependent.
Any help? Thank You.

To do this, you can rely on the magic of the d3 .enter() method. This is a data that operates on a selection, and returns a placeholder for every piece of data assigned to the selection that doesn't currently map to an SVG element in the DOM.
What this means is that if you modify the data, you can then append that data to a selection and have any changes in the data represented by changes in the selection. If you want to add a node to your data, it would work like this:
//Modify data
nodes.push({x : d3.mouse(svg)[0], y : d3.mouse(svg)[1]});
//Modify data of node selection
node.data(nodes);
//Operate on new nodes
node.enter()
.append('circle')
.attr('class','node')
.attr('r', width/25)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
.call(force.end);
Since your first two data points already have DOM nodes assigned to them from when you first created them, this will only append a new circle element for the new data point.
All of this would be called from within an event handler for mousedown or click, and would take user input in the form of mouse position.

Related

Underline node text on mouse over

I have a graph made with d3.js and I have the following attributes and properties for the nodes:
// Enter any new nodes at the parent's previous position
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
.on("click", click)
.on("dblclick", dblclick)
I would like to add the ability to underline the node title when hovering over it. Something like this which unfortunately doesn't work:
var nodeEnter = node.enter().append("g")
.on("mouseover").style("text-decoration","underline")
.on("mouseout").style("text-decoration","none")
EDIT: I would prefer to put a condition to make this happen only for some nodes of the graph.
You aren't using the selection.on() method correctly. In order to do something on an event you need to provide the method with a second parameter: a function that describes the action taken on the event:
D3v6+
.on("mouseover", function(event, datum) { ... })
D3v5 and before
.on("mouseover", function(datum, index, nodes) { ... })
In all versions of D3 this will be the target element (unless using arrow notation). The datum is the data bound to the target element, one item in the array passed to selection.data().
If you only provide one parameter it returns the current event handling function assigned to that event. In your case you likely haven't done this already (because you are attempting to do so), so .on("mouseover").style(...) will return an error such as "Cannot find property style of null" because .on("mouseover") will return null: there is no event handling function assigned to this event to return.
So, to highlight nodes on mouseover with some logic so we can have different outcomes for different nodes, we can use something like:
selection.on("mouseover", function(event, datum) {
if(datum.property == "someValue") {
d3.select(this).style("text-decoration","underline");
}
})
.on("mouseout", function(event,datum) {
d3.select(this).style("text-decoration","none");
})
Where the if statement can be replaced with whatever logic you prefer.
I see you are probably using a hierarchical layout generator, D3's hierarchical layout generators nest the original datum's properties into a data property so that layout properties and non layout properties do not collide, so datum.property may reside at datum.data.property (log the datum if you are having trouble).
var svg = d3.select("body")
.append("svg");
var data = [
"Underline on mouse",
"Never underline"
];
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", 20)
.attr("y", (d,i)=>i*50+40)
.text(d=>d)
.on("mouseover", function(event, datum) {
if(datum == "Underline on mouse") {
d3.select(this).style("text-decoration","underline");
}
})
.on("mouseout", function(event,datum) {
d3.select(this).style("text-decoration","none");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
You can add an underline on hover using CSS
.node:hover{
text-decoration: underline;
}

Overriding the CSS causes D3JS tree path link to be visible after node expansion or collapse

I have code similar to the example in this plunkr.
I am trying to override the ".link" entry in the CSS to download as SVG document.
.link {fill: none; stroke: #ccc; stroke-width: 1.5px;}
The problem is that once I do that, the links are not removed on node expansion or collapse.
I commented the class attribute in the original code and inserted the attributes in the following function:
// Update the linksâ¦
var link = svg.selectAll("path.link")
.data(links, function (d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().append("path", "g")
//.attr("class", "link")
.attr("stroke", "#ccc")
.attr("fill", "none")
.attr("stroke-width", "2px")
.attr("x", boxWidth )
.attr("y", boxHeight)
.attr("d", function (d) {
console.log(source)
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
Any insight as to why this behavior is occurring?
Your original code is:
var link = svg.selectAll("path.link")
...
link.enter()
.append("path")
.attr("class","link")
...
If you remove the class of the appended items, then every time you draw the tree the first selection (link) will be empty (because there are no paths with class link). So, any links in the data array will be entered. This is why the code works as expected initially. However, no link will ever be updated or exited because there are no elements in the selection to either update or exit - the selection will always be empty.
If these are your only paths, you could change the selection selector to be:
var link = svg.selectAll("path")
...
Eg.
Or, alternatively, you could keep the class associated with the elements, but remove any css styling associated with that class. In either event, you'd apply the styling properties with .attr() still, as you have done.

Can't draw barchart using d3.js in React

I am trying to draw barchart with each bar next to its corresponding checkbox. I have several <div className="geneCountBar">s and I try to select all of them, append the data and then draw bars using the following function:
createBarChart = () => {
let datum = this.props.datum.map((geneObj) => {
return geneObj["values"].length;
});
d3.selectAll("div.geneCountBar")
.data(datum)
.enter()
.append("div")
.attr("class", "bar")
.attr('transform', 'rotate(-90)')
.style("height", function(d) {
var barHeight = d * 5;
return barHeight + "px";
});
}
I just took it from the most basic tutorial on barchart creation with d3:
http://alignedleft.com/tutorials/d3/making-a-bar-chart
Somehow after the function runs - I trigger it with the button when the DOM has already been rendered - no DOM manipulation happens at all. I checked datum and it is correct: just an array of several values. The number of elements in the datum corresponds to the number of the selected divs. I checked whether d3.selectAll(div.geneCountBar) actually selects the right elements and it is. The DOM looks like that:
To further clarify what I actually want to achieve. Here you can see checkboxes:
Next to each one of them I want to draw a bar which would represent the amount of each item present in the dataset.
What am I doing wrong here? Any suggestions will be greatly appreciated.
On this case you are not using svg. Need to Style your divs. Try this:
Remove Transform, style width intead heigh
Style background-color for div,
add & nbsp; to prevent colapse empty div
change
createBarChart = () => {
let datum = this.props.datum.map((geneObj) => {
return geneObj["values"].length;
});
d3.selectAll("div.geneCountBar")
.data(datum)
.enter()
.append("div")
.attr("class", "geneCountBar") // <----CHANGE class
.style("background-color","red")
.style("width", function(d) { return (d*5)+"px";})
.html(" ")
}
Every bar must look like this:
<div class="geneCountBar" style="backgroud-color:red;width:50px;"> </div>
Or, on CSS define .geneCountBar{background-color:red;}
<div class="geneCountBar" style="width:50px;"> </div>
I've not access to your rig. Here a working code.
var myData=[15, 30, 20, 10]
var graph = d3.select("#graph")
graph.selectAll("div#graph")
.data(myData) // maybe this's data(myData), not data(datum)
.enter()
.append("div")
.attr("class", "geneCountBar") // <----CHANGE class
.style("width", function(d) {return (d*5)+"px";})
.html(" ")
.geneCountBar {
background-color: red;
margin:3px}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="graph">
</div>
Hope this help
I ended up drawing bars without the use of d3s entering the data.
I put the following divs on the page:
<div className="geneCountBar"
style={{width: this.getHeightForGeneBar(idx, innerIdx), height:"10px",
backgroundColor: getColorForGene(this.getGeneIdx(idx, innerIdx)),
borderRadius: "2px"}}> </div>
And set up individual bars height and color in code with the two functions: getHeightForGeneBar(idx, innerIdx) and getColorForGene(this.getGeneIdx(idx, innerIdx)).

D3.js: Trigger an event in a different chart

I have two identical charts. The graphics for them are built like so:
circles.enter().append("circle")
.attr("r", 0)
.attr("fill", function(d) { return fill_color; })
.attr("class", function(d) { return "circle_" + d.id; })
.on("mouseover", function(d, i) { build_tooltip(d, i, this); })
.on("mouseout", function(d, i) { hide_tooltip(d, i, this); });
On mouseover, it triggers the following function:
build_tooltip = function(data, i, element) {
var content = "Title: " + data.title;
show_tooltip(content, d3.event);
}
My question is: How can I make it so mousing over a circle in Chart #1 triggers the same mouseover event in Chart #2, but with unique data for each chart? Chart #2 must generate its own set of data (in this example, just a title). So, how can I make Chart #2's mouseover event fire whenever Chart #1's does?
In jQuery, this would be quite simple -- there is a literal .trigger() event. But how can I go about accomplishing the same with D3?
Have you tried using D3's dispatch? If not, see through this example for more details on how to use it.

How to display names next to bubbles in d3js motion chart

In Mike Bostocks example http://bost.ocks.org/mike/nations/ there is so much data that putting the names of the countries there would make it chaotic, but for a smaller project I would like to display it.
I found this in the source:
var dot = svg.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(interpolateData(2004))
.enter().append("circle")
.attr("class", "dot")
.style("fill", function(d) { return color(d); })
.text(function(d) { return d.name; })
.call(position)
.sort(order);
dot.append("title")
.text(function(d) { return d.name; });
But somehow a title never shows up. Does anybody have an idea, how to display the name, next to the bubble?
As the other answer suggests, you need to group your elements together. In addition, you need to append a text element -- the title element only displays as a tooltip in SVG. The code you're looking for would look something like this.
var dot = svg.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(interpolateData(2004))
.enter()
.append("g")
.attr("class", "dot")
.call(position)
.sort(order);
dot.append("circle")
.style("fill", function(d) { return color(d); });
dot.append("text")
.attr("y", 10)
.text(function(d) { return d.name; });
In the call to position, you would need to set the transform attribute. You may have to adjust the coordinates of the text element.
Unfortunately grouping the text and circles together will not help in this case. The bubbles are moved by changing their position attributes (cx and cy), but elements do not have x and y positions to move. They can only be moved with a transform-translate. See: https://www.dashingd3js.com/svg-group-element-and-d3js
Your options here are:
1) rewrite the position function to calculate the position difference (change in x and change in y) between the elements current position and its new position and apply that to the . THIS WOULD BE VERY DIFFICULT.
or 2) Write a parallel set of instructions to setup and move the tags. Something like:
var tag = svg.append("g")
.attr("class", "tag")
.selectAll(".tag")
.data(interpolateData(2004))
.enter().append("text")
.attr("class", "tag")
.attr("text-anchor", "left")
.style("fill", function(d) { return color(d); })
.text(function(d) { return d.name; })
.call(tagposition)
.sort(order);
You will need a separate tagposition function since text needs 'x' and 'y' instead of 'cx', 'cy', and 'r' attributes. Don't forget to update the "displayYear" function to change the tag positions as well. You will probably want to offset the text from the bubbles, but making sure the text does not overlap is a much more complicated problem: http://bl.ocks.org/thudfactor/6688739
PS- I called them tags since 'label' already means something in that example.
you have to wrap the circle element and text together , it should look like
<country>
<circle ></circle>
<text></text>
</country>

Categories