d3.js How to target and change elements' attributes - javascript

I'm creating a bunch of Objects (using a pseudo-class, so they all have the same structure) with the properties "name", "type", "status", "poxitionX" and "positionY".
Then I activate my SVG drawing area using d3.js
var svg = d3.select("#d3canvas")
.append("svg")
.attr("width", 600)
.attr("height", 400);
and draw my Objects as circles on the canvas
function drawCircle (objectHandle) {
var tempDrawVar = svg.append("circle")
.style("stroke", "white")
.style("fill", "orange")
.attr("r", 20)
.attr("cx", objectHandle.positionX)
.attr("cy", objectHandle.positionY)
.on("click", function(d){
objectHandle.doStuff();
});
}
doStuff() is a method / prototype function that is supposed to ask the user for input and react to the user input by changing attributes of some of the previously created circles.
The problem is, that I don't know how to "target" those circles. I can update the properties in the Objects just fine, but I really don't think completely deleting the canvas area and creating new circles with every "update" is anywhere near a decent solution.
I can't use tempDrawVar outside the function, and even if I could it would be overwritten each time a new circle is draw anyway (or is it? I'm not sure, I admit).
I tried creating an global Array, using the draw function's parameter as index and using that as the variable instead of tempDrawVar. The drawing function works, but the array stays empty...
var circleArray = new Array();
function drawCircle (objectHandle) {
circleArray[objectHandle] = svg.append("circle")
...
Can anybody point me in the right direction? (In a nutshell: How can I create a function that targets a specific "item" created with d3.js and change one or more of its attributes?)

There are a few options for identifying specific elements. You can use a DOM selector, which means that you would need something like an ID or a class attached to the element to target it.
d3.selectAll("circle").data(data).enter()
.append("circle")
.attr("id", function(d) { return d.id; })
.attr("class", function(d) { return d.class; });
// select DOM element for first data element based on ID
d3.select("#" + data[0].id);
// select by class
d3.select("circle." + data[0].class);
Alternatively, you can use D3's data binding to do the matching. This relies on having a matching function that tells D3 how to match data to DOM elements.
// second argument to .data() is the matching function
d3.selectAll("circle")
.data(data, function(d) { return d.id; })
.enter()
.append("circle");
// select the DOM element for the first data element
d3.selectAll("circle")
.data([data[0]], function(d) { return d.id; });
The latter is probably the more D3 way to do it, but more code (and not particularly nice code at that) to select the element. You also have to remember to pass the same matching function to .data() all the time.
In the example code you've posted, it doesn't look as if you're binding any data to the created DOM elements, so you would have to assign an ID or a class to be able to identify them.

Related

D3JS making a bar chart where each row/data point contains multiple time segments

I'm trying to make a chart of sleeping sessions that looks something like this:
https://i.stack.imgur.com/Wsnha.png
I currently have only been able to get it to draw one rectangle per data point. I'm wondering what the syntax would be to have it draw multiple rectangles associated with a single row/data point since each data point contains an array of sleeping sessions from that day.
Here is what my code currently looks like:
var start;
var end;
var rectGrp = svg
.append("g")
.attr('transform', 'translate(' +padding.left+','+padding.top+')')
.selectAll("rect")
.data(sleepArr)
.enter()
//I want to create one of these rectangles for all the sleeping sessions in that day
.append("rect")
.attr("x", (d) => {
start = d.sessions[0].startTime;
end = d.sessions[0].endTime;
return xScale(start);
})
.attr("width", (d) => {
start = d.sessions[0].startTime;
end = d.sessions[0].endTime;
var width = end-start;
return xScale(width);
})
.attr("y", (d) => yScale(d.date))
.attr("height", yScale.bandwidth())
.attr("fill", (d) => colors[getDayOfWeek(d.date)])
.append("title")
.text((d) => d.date);
I realize that I probably don't have a good understanding of how to use D3 yet and any help would be greatly appreciated.
With the enter/update/exit cycle one item in the data array should be bound to one element in the DOM. The enter selection is used to ensure we create one element for every item for which there is already not one corresponding element in the DOM. In your case the number of rectangles you want to draw is not the same as the number of people in your data array. The approach you have is great for a flat data structure, but we need to make nested selectAll statements in order to properly represent your data.
First, each item in sleepArr represents one person, so lets create one parent g for every person:
var sleepers = svg
.append("g")
.attr('transform', 'translate(' +padding.left+','+padding.top+')')
.selectAll("rect")
.data(sleepArr)
.enter()
.append("g")
.attr("transform", (d,i)=>...
We probably want to do the vertical positioning here as all children of each parent g elements are the same.
Second, we want to create child rectangles representing sleep sessions. We want one rectangle per session. The data array for these rectangles is the sessions property of the datum bound to each parent g, so we pass that to the data method:
sleepers.selectAll("rect")
.data(function(d) return d.sessions; }))
.enter()
.append("rect")
.attr("x", ...
...
For each parent g, representing a single person, we select all child rectangles of each g, bind data, and enter the required rectangles in the appropriate g.
This way we keep a one to one relationship between data and elements representing them: one g per person, one rect per session. The items in sleepArr are bound to the parent g elements, and the items in the child data array are bound to the rects.

Is there a way to add different shapes depending on different data to my force-directed graph?

I'm sure this has been covered in another question on here however, after trying many different examples on my code i cannot seem to get this to work.
I am trying to make the nodes on my force-directed graph a different shape depending on the name of the node, for example, if the node is named 'Switch' it should be displayed as a square.
I have worked out the set up for changing the colour of the nodes based on the data and would like a similar thing for the shape but can't get it to work using d3.v4.
Any help?
var color = d3.scaleOrdinal(d3.schemeCategory10);
var shape = d3.symbolTypes;
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 15)
.attr("fill", function(d) { return color(d.group); })
.attr("d", d3.symbol()
.type(function (d) { return shape(d.name);}))
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
Many Thanks
Faye
You can't use:
var shape = d3.symbolTypes;
....
shape(d.name);
since d3.symbolTypes is not defined in d3 v4 (it was in some versions of v3), in v4 use d3.symbols which is an array not a function.
But, as with color, you could create an ordinal scale for shape:
var color = d3.scaleOrdinal(d3.schemeCategory20);
var shape = d3.scaleOrdinal(d3.symbols);
Now all you have to do is append that shape:
.append('path')
.attr("d", d3.symbol().type( function(d) { return shape(d[property]);} ) );
Since you were appending circles, and are now appending paths, you'll need to change .append('circle'), and as circles have cx cy elements, you need to change to a transform where you set their position.
Here is a bl.ock which should show this in practice, based on MBostock's force directed graph (here)
Keep in mind there are only seven shapes in the d3.symbols array.
Edit:
If you want to specify which shapes get displayed for each node based on a property (rather than letting the ordinal scale set the shape), you could add a property to your data which contains the name of a shape (eg: d3.symbolCross), or create a function which takes in a data value and outputs the name of a symbol. But an ordinal scale is easiest.

Messy data on update pack layout

I'm trying to remove a circle and its subnodes from a pack layout and recalculate the others. I'm using exit to handle removed data, but whenever I add or remove an element and apply the transition, its context becomes messy:
I can see that before Erlang was a leaf, and later it become the parent of Clipper and Basic (!?). Here is the demo.
I've created a pop function that removes the last element:
window.pop = function() {
data.children.pop();
var selection = svg.datum(data).selectAll(".node").data(pack.nodes);
// Removed nodes
selection
.exit()
.remove();
// Update it all
selection
.transition()
.duration(500)
.attr("transform", function(d) {
return translate(d.x, d.y);
})
.select("circle")
.attr("r", function(d) {
return d.r;
});
};
And also an update function that adds an element.
I imagine that, if the text is having the behavior, it should be because the data got messy, because the text is relatively positioned. The poorly children are being lost from their parents! Why is this happening?
The problem is that D3 doesn't match the right data items with the right elements. You can tell it how to by providing a key function as the second argument to .data():
var selection = svg.datum(data).selectAll(".node")
.data(pack.nodes, function(d) { return d.name; });
Complete demo here.

D3 update circle-pack data new nodes overlap existing nodes

I'm following the General Update Pattern but having an issue with regards to layering.
Using a circle-pack layout, I pack the new data, update, enter and exit the circle elements. However, when new elements enter, they overlap the updated circles.
Data key function is based on element name:
.data(nodes, function(d, i) { return d.name; });
So my circle pack has a spot for the updated circle (of the correct location and size) but it's hidden behind its newly entered parent circle.
Is there a way to send these updated nodes to the front or redraw them over the entered circles?
--UPDATE--
As suggested by the person who closed this issue, I've tried implementing the linked to solution using moveToFront.
I added the following code in my update section (which didn't change anything) and then tried adding it after the enter and exit code, which also didn't make any difference.
.each("end", function(d){ d3.select(this).moveToFront(); });
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
For clarity, this is what the selection and update looks like:
// Load data into svg, join new data with old elements, if any.
var nodes = pack.nodes(postData);
node = root = postData;
groupNodes = svg.selectAll("g")
.data(nodes, function(d, i) { return d.name; });
// Update and transition existing elements
groupNodes.select("circle")
.transition()
.duration(duration)
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.attr('r', function(d) { return d.r; })
.each("end", function(d){ d3.select(this).moveToFront(); });
This moveToFront code does not make a difference to my output, and the updated circles remain behind the entered selection circles.
To summarize: the issue seems to be caused by a hierarchy layout (circle-packing) which expects the circles to be drawn in the order of the data's hierarchy. The d3 update pattern (using enter, update and exit selections) causes selected update elements to remain in the svg when the hierarchy is re-drawn, and the new layers are drawn over it. The parents of those nodes are already correctly set, so parentNode.appendChild doesn't do anything in this case, because it's not the cause of the issue.
Here is a fiddle to demonstrate my issue. I've tried putting the moveToFront code in various places, with no visible difference.
When you hit the "Change Data" button, it'll redraw the circles, but any circles whose names overlap between the two data sets are not nested properly in the circle-pack. Children of "Group A" are hidden behind one of the parent circles. You can verify the nodes are there via Inspect Element.
Another pic from the updated fiddle:
D3 provides a way to reorder elements based on the data bound to them with the .sort() function. In your case, the condition to check is the .depth attribute of the elements -- "deeper" elements should appear in front:
svg.selectAll("g")
.sort(function (a, b) {
if (a.depth < b.depth) return -1;
else return 1;
});
Complete demo here.

Appending elements to d3 selection

In a d3 program, I have a many circle elements with data bound to them. In a callback to an event, I want to filter out most of them and place a larger red circle on top of those that are left. It looks something like this:
svg.selectAll("circle")
.filter(/* filter function not important */)
.append("circle")
.attr("class", "extra")
.attr("cx", function(){return d3.select(this).attr("cx")})
.attr("cy", function(){return d3.select(this).attr("cy")})
.attr("r", 5);
And the result is this (the first line is the original circle):
<circle cx="55.41208075590415" cy="279.3650793650794" r="1">
<circle class="extra" r="5"></circle>
</circle>
So I have two problems. One, it's inserting as a child of the old element, rather than a sibling. Ideally it would be inserted as the last child of svg. Second, cx and cy aren't getting copied over. What magic do I have to utter to make this work?
If you are okay overwriting existing elements, see the answer below. To create new ones, here's how.
svg.selectAll("circle")
.filter(/* filter function not important */)
.each(function(d){
svg.append("circle")
.attr("class", "extra")
.attr("cx", function() { return x(d.fop) })
.attr("cy", function() { return y(d.bar) })
.attr("r", 5);
})
Notice two things. One, the inner functions do not take parameters, because there is no data bound to the extra circle. Use the d from the existing circle instead. Two, I couldn't figure out how to access the cx and cy of the existing circle, so I had to recompute them instead, which means the scales x and y must remain unchanged. I'll change the accepted answer if anyone can improve on this.
Instead of doing a .append on your selection you should simply use your filter function to modify the circles whose data have the criterion that pass your filter function. You can modify those circles, instead of appending a new element. So your code would look something like this.
svg.selectAll('circle')
.filter(/** what you want**/)
.attr('r', newRadiusSize)
.attr('fill', 'red')
You could even add a nice transition to make the circles change to red instead of just snapping to the newer bigger circles.

Categories