In this code, http://enjalot.com/inlet/4124664/
of which the main part is:
function render(data) {
var nodz = svg.selectAll(".node")
.data(data);
nodz.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return "id"+d[0]+d[1]; })
.attr("x",0)
.attr("y", 0)
.attr("transform", function(d) {
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
})
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("stroke", "black")
.text(function(d) {return d[2]; });
// update
nodz.selectAll("text")
.text(function(d) {
return d[2]; });
// another go
d3.selectAll("text")
.text(function(d) {
return d[2]; });
}
// data in form [x pos, y pos, value to display]
var nodes = [[0,0,0],[1,1,0],[1, -1,0],[2,0,0], [2,2,0]];
render(nodes);
nodes2 = [[0,0,1],[1,1,1],[1, -1,1],[2,0,1], [2,2,1], [2, -2,1]];
render(nodes2);
I call the code to draw some nodes twice.
I expect it to draw five nodes with a value of zero in the first pass,
Then I add another item to the list and update all the values to 1 so expect to see all the values change to 1 and a new node appear. Instead, I'm only seeing the last one being set to 1. I've tried adding a unique id to bind the node to the data but this isn't working. Also tried reselecting to see if the data is now bound. In all the tutorials I've been through, just calling the selectAll().data() part updates the data, what am I missing?
The second optional argument to .data() is a function that tells d3 how to match elements. That's where you need to compare your IDs, see the documentation. That said, it should work without IDs in your case as it matches by index by default.
The problem with updating the text is that after calling .selectAll() you need to call .data() again to let d3 know what you want to match to that selection (i.e. that the new data should be bound to the old data).
Related
I am attempting to access the data index of a shape on mouseover so that I can control the behavior of the shape based on the index.
Lets say that this block of code lays out 5 rect in a vertical line based on some data.
var g_box = svg
.selectAll("g")
.data(controls)
.enter()
.append("g")
.attr("transform", function (d,i){
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+ 5))+")"
})
.attr("class", "controls");
g_box
.append("rect")
.attr("class", "control")
.attr("width", 15)
.attr("height", 15)
.style("stroke", "black")
.style("fill", "#b8b9bc");
When we mouseover rect 3, it transitions to double size.
g_box.selectAll("rect")
.on("mouseover", function(d){
d3.select(this)
.transition()
.attr("width", controlBoxSize*2)
.attr("height", controlBoxSize*2);
var additionalOffset = controlBoxSize*2;
g_box
.attr("transform", function (d,i){
if( i > this.index) { // want to do something like this, what to use for "this.index"?
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5)+additionalOffset)+")"
} else {
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5))+")"
}
})
})
What I want to do is move rect 4 and 5 on mouseover so they slide out of the way and do not overlap rect 3 which is expanding.
So is there a way to detect the data index "i" of "this" rect in my mouseover event so that I could implement some logic to adjust the translate() of the other rect accordingly?
You can easily get the index of any selection with the second argument of the anonymous function.
The problem here, however, is that you're trying to get the index in an anonymous function which is itself inside the event handler, and this won't work.
Thus, get the index in the event handler...
selection.on("mouseover", function(d, i) {
//index here ---------------------^
... and, inside the inner anonymous function, get the index again, using different parameter name, comparing them:
innerSelection.attr("transform", function(e, j) {
//index here, with a different name -----^
This is a simple demo (full of magic numbers), just to show you how to do it:
var svg = d3.select("svg");
var data = d3.range(5);
var groups = svg.selectAll("foo")
.data(data)
.enter()
.append("g");
var rects = groups.append("rect")
.attr("y", 10)
.attr("x", function(d) {
return 10 + d * 20
})
.attr("width", 10)
.attr("height", 100)
.attr("fill", "teal");
groups.on("mouseover", function(d, i) {
d3.select(this).select("rect").transition()
.attr("width", 50);
groups.transition()
.attr("transform", function(e, j) {
if (i < j) {
return "translate(40,0)"
}
})
}).on("mouseout", function() {
groups.transition().attr("transform", "translate(0,0)");
rects.transition().attr("width", 10);
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: don't do...
g_box.selectAll("rect").on("mouseover", function(d, i){
... because you won't get the correct index that way (which explain your comment). Instead of that, attach the event to the groups, and get the rectangle inside it.
I'm pretty sure d3 passes in the index as well as the data in the event listener.
So try
.on("mouseover", function(d,i)
where i is the index.
Also you can take a look at a fiddle i made a couple months ago, which is related to what you're asking.
https://jsfiddle.net/guanzo/h1hdet8d/1/
You can find the index usign indexOf(). The second argument in the event mouseover it doesn't show the index in numbers, it shows the data info you're working, well, you can pass this info inside indexOf() to find the number of the index that you need.
.on("mouseover", (event, i) => {
let index = data.indexOf(i);
console.log(index); // will show the index number
})
I am trying to make a stacked bar graph through d3js and have it update when new data is passed through an update function. I call this update function to initially call the graph and it works fine. However, when I change the data and call it again, it erases all the "rect" elements from the graph (When I console log the data, it appears to be passing through). How can I make the graph be redrawn appropriately? I have tried experimenting with the .remove() statement at the beginning, but without it the data doesn't pass through when the bars are redrawn.
function update(my_data) {
svg.selectAll(".year").remove();
var year = svg.selectAll(".year")
.data(my_data)
.enter().append("g")
.attr("class", "year")
.attr("transform", function(d) { return "translate(" + x0(d.Year) + ",0)"; });
var bar = year.selectAll(".bar")
.data( function(d){ return d.locations; });
bar
.enter().append("rect")
.attr("class", "bar")
.attr("width", x0.rangeBand())
.attr("y", function(d) { return y(d.y1); })
.attr("height", function(d) { return y(d.y0) - y(d.y1); })
.style("fill", function(d) { return color(d.name); });
}
update(data);
It's hard to tell exactly what you're doing cause your question doesn't include the data or the DOM. It would help if you included a link to a work-in-progress jsFiddle or something.
If I had to guess what's going wrong, it looks like you're doing a nested join where each year gets bound to a g element and then each location gets bound to a rect inside each g element.
The issue is likely you are only specifying the enter behavior, but not the update behavior or the exit behavior. As a result, when you try to redraw, nothing updates and nothing exits - but new data elements will get added.
It would seem that is why you have to add the selectAll().remove() to get anything to redraw. By removing everything, all the data elements will trigger the enter condition and get added again.
Take a look at these tutorials to better understand how the enter/update/exit pattern works and how nested joins work.
General Update Pattern: https://bl.ocks.org/mbostock/3808218
Nested Selections: https://bost.ocks.org/mike/nest/
Also, here is a jsFiddle I wrote some time ago to demonstrate how to use nested selections and the general update pattern together:
https://jsfiddle.net/reblace/bWp8L/
var series = svg.selectAll("g.row").data(data, function(d) { return d.key; });
/*
* This section handles the "enter" for each row
*/
// Adding a g element to wrap the svg elements of each row
var seriesEnter = series.enter().append("g");
seriesEnter
.attr("class", "row")
.attr("transform", function(d, i){
return "translate(" + margin.left + "," + (margin.top + (span*i)) + ")";
})
.attr("opacity", 0).transition().duration(200).attr("opacity", 1);
// Adding a text label for each series
seriesEnter.append("text")
.style("text-anchor", "end")
.attr("x", -6)
.attr("y", boxMargin + (boxDim/2))
.attr("dy", ".32em")
.text(function(d){ return d.key; });
// nested selection for the rects associated with each row
var seriesEnterRect = seriesEnter.selectAll("rect").data(function(d){ return d.values; });
// rect enter. don't need to worry about updates/exit when a row is added
seriesEnterRect.enter().append("rect")
.attr("fill", function(d){ return colorScale(d)})
.attr("x", function(d, i){ return i*span + boxMargin; })
.attr("y", boxMargin)
.attr("height", boxDim)
.attr("width", boxDim);
/*
* This section handles updates to each row
*/
var seriesUpdateRect = series.selectAll("rect").data(function(d){ return d.values});
// rect update (Will handle updates after enter)
// rect enter
seriesUpdateRect.enter().append("rect")
.attr("x", function(d, i){ return i*span + boxMargin; })
.attr("y", boxMargin)
.attr("height", boxDim)
.attr("width", boxDim);
// rect enter + update
seriesUpdateRect
.attr("fill", function(d){ return colorScale(d)});
// Exit
seriesUpdateRect.exit();
/*
* This section handles row exit
*/
series.exit()
.attr("opacity", 1)
.transition().duration(200).attr("opacity", 0)
.remove();
I am using d3 for a bar chart in my application and have a need to annotate each of the bars with a piece of text for the data value the bar represents.
I have this working so far like so:
layers = svg.selectAll('g.layer')
.data(stacked, function(d) {
return d.dataPointLegend;
})
.enter()
.append('g')
.attr('class', function(d) {
return d.dataPointLegend;
});
layers.selectAll('rect')
.data(function(d) {
return d.dataPointValues;
})
.enter()
.append('rect')
.attr('x', function(d) {
return x(d.pointKey);
})
.attr('width', x.rangeBand())
.attr('y', function(d) {
return y(d.y0 + d.pointValue);
})
.attr('height', function(d) {
return height - margin.bottom - margin.top - y(d.pointValue)
});
layers.selectAll('text')
.data(function(d) {
return d.dataPointValues;
})
.enter()
.append('text')
.text(function() {
return 'bla';
})
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function(d) {
return y(d.y0 + d.pointValue) - 5;
})
I'd actually only like to append a text element if a certain property exists in the data.
I have seen the datum selection method in the docs and wondered if this is what I need, I'm unsure how I cancel an append call if the property I'm seeking is not present.
Thanks
Attempt 2
Ok So I have had another stab, this time using the each function like so:
layers.selectAll('text')
.data(function(d) {
return d.dataPointValues;
})
.enter()
//This line needs to be my each function I think?
.append('text')
.each(function(d){
if(d.pointLabel) {
d3.select(this)
.text(function(d) {
return d.pointLabel;
})
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function(d) {
return y(d.y0 + d.pointValue) - 5;
})
.attr('class', 'data-value')
}
});
}
The problem I now have is that I get a text element added regardless of whether a pointLabel property is present.
It feels like I'm close, I did wonder if I should be moving my append('text') down into the each, but when I tried I got an error as d3 was not expecting that particular chain of calls.
How about using d3's data binding for this.... Rather than appending the text to layers.enter() selection, do the following to the entire layers selection, i.e. including the updating nodes:
labels = layers.selectAll('text')
.data(function(d) {
// d is the datum of the parent, and for this example
// let's assume that the presence of `pointLabel`
// indicates whether the label should be displayed or not.
// You could work in more refined logic for it if needed:
return d.pointLabel ? [d] : [];
})
// The result of the above is that if a label is needed, a binding will
// occur to a single element array containing `d`. Otherwise, it'll bind
// to an empty array. After that binding, using enter, update and exit,
// you get to add, update or even remove text (you might need removal if
// you're updating an existing view whose existing label needs to go away)
labels.enter()
.append("text")
labels
.text(function(d) { d.pointLabel })
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', ...)
labels.exit()
.remove()
The trick here (it's hardly a trick, but it's not a very common d3 use-case) is that it's either binding to a single element array [d] or to a blank one [], which is how you get to use the enter() selection to only create labels where needed. And the benefit of this approach over non-data-binding appproaches is that this code can be called multiple times — whenever a d.pointLabel changes or when the app's state change — and the labels' presense (or lack of presence, ie removal) will update accordingly.
I'm trying to animate a data join. I can get the state to change and update but there isn't any seamless animation between states.
I tried to refer to this to answer my question:
d3.js trying to animate data, but changed data always treated as new
This data is created upon json load...and then, on a button click, i'm just simply adding 20 ( for testing purposes ) to my y values...it updates but there is no transition. What am i selecting incorrectly?
The variable bar is already defined globally.
function btnClick(){
//bar.exit().remove();
bar = chart.selectAll("g")
.data(mydata.players)
console.log(bar.data(mydata.players))
bar.exit().remove()
bar.enter().append("g")
.transition()
.duration(1650)
.attr("transform", function (d) {
return "translate(" + x(d.name) + ",0)";
});
bar.append("rect")
.attr('width', x.rangeBand())
.attr("y", function (d) {
return y(d.money+20)
})
.attr("height", function (d) {
return height - y(d.money+20);
})
Trying to move the transition before the height change:
chart.selectAll("g")
.data(mydata.players);
bar.exit().remove();
bar.attr("transform",
function(d) {
return "translate(" + x(d.name) + ",0)";
});
bar.append("rect")
.attr('width', x.rangeBand())
.transition()
.attr("y", function(d) {
return y(d.money + 20)
})
.attr("height", function(d) {
return height - y(d.money + 20);
})
I think i'm just not grabbing the right selection for bar in btnClick(). Maybe I'm not supposed to grab all the 'g' elements? And the bar is defined on json load earlier so I'm not certain I should even have to define it again by selecting all the g elements....if it's already been built. I have stored it and should be able to manipulate it: but if I don't define it, then for some reason the exit.remove() doesn't work.
Ok, I made the small necessary changes to your code to have this working. So, two things to note:
you want to select your rectangles for update:
bar.select("rect")
.attr('width', x.rangeBand())
.transition().duration(750).ease("linear")
.attr("y", function (d) {
return y(d.money + 20)
})
.attr("height", function (d) {
return height - y(d.money + 20);
})
you want to re-set your y domain to accommodate for the new data values (you really don't have a new set of data, you are just adding 20...so, do the same here):
y.domain([0, d3.max(mydata.players, function (d) {
return d.money + 20;
})])
NOTE: your tallest bar will not move since its value puts it at the top of the old and new scales (the domain is based on the data values). To see all bars rise, you can inflate the domain of your original data (say, + 20). I placed a comment where you can do this.
Here is the complete FIDDLE.
I'm using a force directed graph that appends circles to each node.
As part of the node creation, I first set the radius "r" of each node circle to a default and consistent value (defaultNodeSize = 10). This successfully draws a cluster where all related nodes are the same size.
// Append circles to Nodes
node.append("circle")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("r", function(d) { if (d.id==focalNodeID) { return centerNodeSize; } else { return defaultNodeSize; } } ) // <---------------------------- Radius "r" set HERE!!!!
.style("fill", "White") // Make the nodes hollow looking
.attr("type_value", function(d, i) { return d.type; })
.attr("color_value", function(d, i) { return color_hash[d.type]; })
.attr("rSize", function(d, i) { return d.rSize; }) // <------------------ rSize HERE!!!!
.attr("id", "NODE" )
.attr("class", function(d, i) {
var str = d.type;
var strippedString = str.replace(/ /g, "_")
//return "nodeCircle-" + strippedString; })
if (d.id==focalNodeID) { return "focalNodeCircle"; }
else { return "nodeCircle-" + strippedString; }
})
.style("stroke-width", 5) // Give the node strokes some thickness
.style("stroke", function(d, i) { return color_hash[d.type]; } ) // Node stroke colors
.call(force.drag);
Also, upon creation, I set an attribute called "rSize", which specifies that node's absolute magnitude. Each node has a different rSize and rSize is different than the defaultNodeSize. The purpose of rSize is so that I can access it, later, and dynamically change the circle's radius from it's defaultNodeSize to it's rSize (or the reverse) allowing each node to expand or contract, based on controllers.
In a separate controller function, I later select all nodes I want to apply the new rSize to. Selecting them is easy...
var selectedNodeCircles = d3.selectAll("#NODE");
However, I don't know what the syntax is to read each node's rSize and apply rSize to that specific node that's being changed. I would think that it's something like...
selectedNodeCircles.("r", function(){ return this.attr("rSize"); });
In other word's, I'd like to retrieve that specific node's "rSize" attribute value and set the attribute "r" to the value retrieved from "rSize".
Any idea of what the correct syntax is to do this?
Thanks for any help you can offer!
You are looking for the getAttribute() function.
So something like this should work for you:
selectedNodeCircles.attr("r", function() {return this.getAttribute("rSize")})
Remember that this in the function, is the circle itself and hence simply an element in the DOM, to the best of my understanding.
You can confirm this by simply printing out this using console.log(this) right before the result statement.
Hope this helps.