maintaining the layering of elements after adding new elements - javascript

I'm drawing a little clickable graph data browser.
Example:
First, I load a few movies, and I see this:
Then, after I click on one of the nodes (Hellraiser, in this case), I use ajax to load additional related information properties and values, and end up with this:
The lines and circles of the newly added nodes are obviously drawn after the originally clicked node was.
Here is the draw method that gets called every time new data is ready to be added to the graph:
function draw() {
force.start();
//Create edges as lines
var edges = svg.selectAll("line")
.data(dataset.edges)
.enter()
.append("line")
.style("stroke", "#ccc")
.style("stroke-width", 2)
.on("mouseover", lineMouseover)
.on("mouseout", lineMouseout);
//create the nodes
var node = svg.selectAll(".node")
.data(dataset.nodes)
.enter()
.append("g")
.attr("class", "node")
.on("click", callback)
.attr("r", function(d, i) { //custom sizes based on datatype
if(d.datatype && (d.datatype in _design) ) {
return _design[d.datatype].size;
} else {
return _design["other"].size;
}
})
.call(force.drag);
//create fancy outlines on the nodes
node.append("circle")
.attr("r", function(d,i) { //custom sizes based on datatype
if(d.datatype && (d.datatype in _design) ) {
return _design[d.datatype].size * r;
} else {
return _design["other"].size * r;
}
})
.style("stroke", "black")
.style("stroke-width", 3)
.style("fill", function(d, i) { //custom color based on datatype
if(d.datatype && (d.datatype in _design) ) {
return _design[d.datatype].color;
} else {
return _design["other"].color;
}
})
.attr("class","circle");
//Add text to each node.
node.append("text")
.attr("dx", 0)
.attr("dy", ".25em")
//.attr("class", "outline")
.attr("fill", "black")
.text(function(d, i) {
return d.name;//d.name
});
};
How do I go about drawing those lines underneath the clicked node?

You can group the different kinds of elements below g elements that you can create at the beginning in the required order. This way, anything you append to them later will be ordered correctly:
var links = svg.append("g"),
nodes = svg.append("g"),
labels = svg.append("g");
// ...
var edges = links.selectAll("line")
.data(dataset.edges)
.enter()
.append("line");
var node = nodes.selectAll(".node")
.data(dataset.nodes)
.enter()
.append("g")
// etc.

Related

D3 update pattern drawing the same element twice

I’ve created a line graph in D3. To ensure the line doesn’t overlap the y-axis, I have altered the range of the x-axis. As a result of this, there is a gap between the x-axis and the y-axis which I am trying to fill with another line.
The rest of the graph uses the D3 update pattern. However, when I try to use the pattern on this simple line, two path elements are drawn (one on top of the other). I have tried numerous solutions to correct this issue but I’m not having any luck. Does anyone have any suggestions?
The code below is what is drawing two of the same path elements
var xAxisLineData = [
{ x: margins.left , y: height - margins.bottom + 0.5 },
{ x: margins.left + 40, y: height - margins.bottom + 0.5 }];
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var update = vis.selectAll(".xAxisLine")
.data(xAxisLineData);
var enter = update.enter()
.append("path")
.attr("d", xAxisLine(xAxisLineData))
.attr("class", "xAxisLine")
.attr("stroke", "black");
Your problem is here:
var update = vis.selectAll(".xAxisLine")
.data(xAxisLineData);
this is a null selection, assuming there are no elements with the class xAxisLine, which means that using .enter().append() will append one element for each item in the xAxisLineData array.
You want to append one path per set of points representing a line, not one path for each in a set of points representing a line.
You really just want one line to be drawn, so you could do:
.data([xAxisLineData]);
or, place all the points in an array when defining xAxisLineData
Now you are passing a data array to the selection that contains one item: an array of points - as opposed to many items representing individual points. As the data array has one item, and your selection is empty, using .enter().append() will append one element:
var svg = d3.select("body").append("svg").attr("width",500).attr("height",400);
var lineData = [{x:100,y:100},{x:200,y:200}]
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var colors = ["steelblue","orange"];
var line = svg.selectAll(null)
.data([lineData])
.enter()
.append("path")
.attr("d", xAxisLine(lineData))
.attr("class", "xAxisLine")
.attr("stroke-width", function(d,i) { return (1-i) * 10 + 10; })
.attr("stroke", function(d,i) { return colors[i]; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Compare without using an array to hold all the data points:
var svg = d3.select("body").append("svg").attr("width",500).attr("height",400);
var lineData = [{x:100,y:100},{x:200,y:200}]
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var colors = ["steelblue","orange"];
var line = svg.selectAll(null)
.data(lineData)
.enter()
.append("path")
.attr("d", xAxisLine(lineData))
.attr("class", "xAxisLine")
.attr("stroke-width", function(d,i) { return (1-i) * 10 + 10; })
.attr("stroke", function(d,i) { return colors[i]; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
But, we can make one last change. Since each item in the data array is bound to the element, we can reference the datum, not the data array xAxisLineData, which would make adding multiple lines much easier:
.attr("d", function(d) { return xAxisLine(d) })
Note in the demo below that the variable xAxisLineData is defined as an array of arrays of points, or an array of multiple lines.
var svg = d3.select("body").append("svg").attr("width",500).attr("height",400);
var lineData = [[{x:100,y:100},{x:200,y:200}],[{x:150,y:150},{x:260,y:150}]]
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var colors = ["steelblue","orange"];
var line = svg.selectAll(null)
.data(lineData)
.enter()
.append("path")
.attr("d", function(d) { return xAxisLine(d) }) // use the element's datum
.attr("class", "xAxisLine")
.attr("stroke-width", function(d,i) { return (1-i) * 10 + 10; })
.attr("stroke", function(d,i) { return colors[i]; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

How to manipulate nodes based on dynamicaly changing text? (enter/exit/update)

I am using d3.js with the force layout. Now, with the help of the dynamically changing array data it is possible to highlight nodes dynamically based on the array. Also with the code below i am able to show up dynamically the names of the nodes, which are part of the array, as a text.
So, when the array has for example 3 entries, then 3 nodes are shown up and also 3 names of the nodes appear. Let's say their names are "a", "b", "c", so the text "a", "b", "c" appears on screen.
Now, when i click on the new appeared text "a", then i want the node, which contains that name, to be filled green. I tried this with the function called specialfunction. The problem is, that all nodes fill green when i click
on the text "a". Can someone of you guys maybe help? Thanks.
var texts = svg.selectAll(".texts")
.data(data);
textsExit = texts.exit().remove();
textsEnter = texts.enter()
.append("text")
.attr("class", "texts");
textsUpdate = texts.merge(textsEnter)
.attr("x", 10)
.attr("y", (d, i) => i * 16)
.text(d => d.name)
.on("click", specialfunction);
function specialfunction(d) {
node.style("fill", function(d){ return this.style.fill = 'green';});
};
Right now, your specialFunction function is only taking the nodes selection and setting the style of all its elements to the returned value of...
this.style.fill = 'green';
... which is, guess what, "green".
Instead of that, filter the nodes according to the clicked text:
function specialFunction(d) {
nodes.filter(function(e) {
return e === d
}).style("fill", "forestgreen")
}
In this simple demo, d is the number for both texts and circles. Just change d in my demo to d.name or any other property you want. Click the text and the correspondent circle will change colour:
var svg = d3.select("svg");
var data = d3.range(5);
var nodes = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 50)
.attr("cx", function(d) {
return 30 + d * 45
})
.attr("r", 20)
.style("fill", "lightblue")
.attr("stroke", "gray");
var texts = svg.selectAll(null)
.data(data)
.enter()
.append("text")
.attr("y", 88)
.attr("x", function(d) {
return 26 + d * 45
})
.attr("fill", "dimgray")
.attr("cursor", "pointer")
.text(function(d) {
return d
})
.on("click", specialFunction);
function specialFunction(d) {
nodes.filter(function(e) {
return e === d
}).style("fill", "forestgreen")
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
EDIT: Answering your comment, this even simpler function can set the circles to the original colour:
function specialFunction(d) {
nodes.style("fill", function(e){
return e === d ? "forestgreen" : "lightblue";
})
}
Here is the demo:
var svg = d3.select("svg");
var data = d3.range(5);
var nodes = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 50)
.attr("cx", function(d) {
return 30 + d * 45
})
.attr("r", 20)
.style("fill", "lightblue")
.attr("stroke", "gray");
var texts = svg.selectAll(null)
.data(data)
.enter()
.append("text")
.attr("y", 88)
.attr("x", function(d) {
return 26 + d * 45
})
.attr("fill", "dimgray")
.attr("cursor", "pointer")
.text(function(d) {
return d
})
.on("click", specialFunction);
function specialFunction(d) {
nodes.style("fill", function(e){
return e === d ? "forestgreen" : "lightblue";
})
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

Converting only certain nodes in D3 Sankey chart from rectangle to circle

I would like to reproduce the process from D3 Sankey chart using circle node instead of rectangle node, however, I would like to select only certain nodes to change from rectangles to circles.
For example, in this jsfiddle used in the example, how would you only select Node 4 and Node 7 to be converted to a circle?
I updated your fiddle.
Basically you just need some way to select the nodes that you want to make different. I used unique classname so that you can style them with CSS as well. I didn't feel like writing the code to select just 4 and 7 (I'm lazy) so I just selected all of the even nodes instead.
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", function (d, i) { return i % 2 ? "node rect" : "node circle";
})
Then you can use that to select the nodes and add circles instead of rectangles.
svg.selectAll(".node.circle").append("circle")
.attr("r", sankey.nodeWidth() / 2)
.attr("cy", function(d) { return d.dy/2; })
.attr("cx", sankey.nodeWidth() / 2)
.style("fill", function (d) {
There is also another similar approach, illustrated in the following jsfiddle.
I started from this fiddle (from another SO question that you merntioned)), where all nodes had already been converted to circles:
Then I modified existing and added some new code that involves filtering during creation of circles:
// add the circles for "node4" and "node7"
node
.filter(function(d){ return (d.name == "node4") || (d.name == "node7"); })
.append("circle")
.attr("cx", sankey.nodeWidth()/2)
.attr("cy", function (d) {
return d.dy/2;
})
.attr("r", function (d) {
return Math.sqrt(d.dy);
})
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add the rectangles for the rest of the nodes
node
.filter(function(d){ return !((d.name == "node4") || (d.name == "node7")); })
.append("rect")
.attr("y", function (d) {
return d.dy/2 - Math.sqrt(d.dy)/2;
})
.attr("height", function (d) {
return Math.sqrt(d.dy);
})
.attr("width", sankey.nodeWidth())
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
Similar code had to be modified for accurate positioning text beside rectangles.
I believe the final result looks natural, even though it lost some of the qualities of the original Sankey (like wider connections).

Add images to d3 chord diagram

I would like images as the endpoints. I have tried adding but no luck. Any ideas/working examples?
http://bost.ocks.org/mike/uberdata/
Each neighborhood in that example is given a <g> element with a class of group.
// Add a group per neighborhood.
var group = svg.selectAll(".group")
.data(layout.groups)
.enter().append("g")
.attr("class", "group")
.on("mouseover", mouseover);
This is the element to which the text label and the endpoint path are appended.
// Add the group arc.
var groupPath = group.append("path")
.attr("id", function(d, i) { return "group" + i; })
.attr("d", arc)
.style("fill", function(d, i) { return cities[i].color; });
// Add a text label.
var groupText = group.append("text")
.attr("x", 6)
.attr("dy", 15);
You could append each image to this group also, using an svg <image> element. If, for example, your dataset contains the urls for your images, you might do the following:
var groupImage = group.append("image")
.attr("xlink:href", function(d) {return d.image_url;})

d3js: adding same type of elements with different data

//add circles with price data
svgContainer.selectAll("circle")
.data(priceData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y1(d); })
//add circles with difficulty data
svgContainer.selectAll("circle")
.data(difficultyData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
In the first half, circles with price data are added along the relevant line in the graph chart. Now I want to do the same with the second half to add circles with different data to a different line. However, the first circles' data are overwritten by the second circles' data, and the second circles never get drawn.
I think I have a gut feeling of what's going on here, but can someone explain what exactly is being done and how to solve the problem?
possible reference:
"The key function also determines the enter and exit selections: the
new data for which there is no corresponding key in the old data
become the enter selection, and the old data for which there is no
corresponding key in the new data become the exit selection. The
remaining data become the default update selection."
First, understand what selectAll(), data(), enter() do from this great post.
The problem is that since circle element already exists by the time we get to the second half, the newly provided data simply overwrites the circles instead of creating new circles. To prevent this from happening, you need to specify a key function in data() function of the second half. Then, the first batch of circles do not get overwritten.
//add circles with price data
svgContainer.selectAll("circle")
.data(priceData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y1(d); })
//add circles with difficulty data
svgContainer.selectAll("circle")
.data(difficultyData, function(d) { return d; }) // SPECIFY KEY FUNCTION
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
you can append the circles in two different groups, something like:
//add circles with price data
svgContainer.append("g")
.attr("id", "pricecircles")
.selectAll("circle")
.data(priceData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y1(d); })
//add circles with difficulty data
svgContainer.append("g")
.attr("id", "datacircles")
.selectAll("circle")
.data(difficultyData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
if the circles are in different groups they won't be overwritten
I had the same question as OP. And, I figured out a solution similar to tomtomtom above. In short: Use SVG group element to do what you want to do with different data but the same type of element. More explanation about why SVG group element is so very useful in D3.js and a good example can be found here:
https://www.dashingd3js.com/svg-group-element-and-d3js
My reply here includes a jsfiddle of an example involving 2 different datasets both visualized simultaneously on the same SVG but with different attributes. As seen below, I created two different group elements (circleGroup1 & circleGroup2) that would each deal with different datasets:
var ratData1 = [200, 300, 400, 600];
var ratData2 = [32, 57, 112, 293];
var svg1 = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 400);
var circleGroup1 = svg1.append("g");
var circleGroup2 = svg1.append("g");
circleGroup1.selectAll("circle")
.data(ratData1)
.enter().append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30; })
.attr("r", function(d) { return Math.sqrt(d); });
circleGroup2.selectAll("circle")
.data(ratData2)
.enter()
.append("circle")
.attr("r", function(d, i){
return i*20 + 5;
})
.attr("cy", 100)
.attr("cx", function(d,i){ return i*100 +30;})
.style('fill', 'red')
.style('fill-opacity', '0.3');
What is happening is that you are:
In the FIRST HALF:
getting all circle elements in the svg container. This returns nothing because it is the first time you're calling it so there are no circle elements yet.
then you're joining to data (by index, the default when you do not specify a key function). This puts everything in the priceData dataset in the "enter" section.
Then you draw your circles and everything is happy.
then, In the SECOND SECTION:
You are again selecting generically ALL circle elements, of which there are (priceData.length) elements already existing in the SVG.
You are joining a totally different data set to these elements, again by index because you did not specify a key function. This is putting (priceData.length) elements into the "update section" of the data join, and either:
if priceData.length > difficultyData.length, it is putting (priceData.length - difficulty.length) elements into the "exit section"
if priceData.length < difficultyData.length, it is putting (difficulty.length - priceData.length) elements into the "enter section"
Either way, all of the existing elements from the first "priceData" half are reused and have their __data__ overwritten with the new difficultyData using an index-to-index mapping.
Solution?
I don't think a key function is what you are looking for here. A key function is way to choose a unique field in the data on which to join data instead of index, because index does not care if the data or elements are reordered, etc. I would use this when i want to make sure a single data set is correctly mapped back to itself when i do a selectAll(..).data(..).
The solution I would use for your problem is to group the circles with a style class so that you are creating two totally separate sets of circles for your different data sets. See my change below.
another option is to nest the two groups of circles each in their own "svg:g" element, and set a class or id on that element. Then use that element in your selectAll.. but generally, you need to group them in some way so you can select them by those groupings.
//add circles with price data
svgContainer.selectAll("circle.price")
.data(priceData)
.enter()
.append("svg:circle")
.attr("class", "price")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
//add circles with difficulty data
svgContainer.selectAll("circle.difficulty")
.data(difficultyData)
.enter()
.append("svg:circle")
.attr("class", "difficulty")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
Using this method, you will always be working with the correct circle elements for the separate datasets. After that, if you have a better unique value in the data than simply using the index, you can also add a custom key function to the two .data(..) calls.

Categories