d3.js does not update one specific property when data changes - javascript

I have 4 circles with a radius of 70px upon enter.
function draw_circles(...){
data = get_random_generador_data();
//the data contains the circle coordinates, as well
// as the radius, the text or the fill color.
var circleGroup = svgContainer.selectAll('g').data(data);
//=======
//ENTER
//=======
var circleGroupEnter = circleGroup
.enter()
.append('g')
.attr("id", function(d) { return "group_"+d["ix"]; })
circleGroupEnter
.append("circle")
.attr("cx", function(d) { return d["cx"]; })
.attr("cy", function(d) { return d["cy"]; })
.attr("r", function(d) { return d["rad"]; })
.attr("id", function(d) { return "circle_"+d["ix"]; }) //this is 70
.style("fill", function(d) { return d["fill_color"]; })
;
//=======
//UPDATE
//=======
circleGroup.select("circle")
.attr("cx", function(d) { return d["cx"]; })
.attr("cy", function(d) { return d["cy"]; })
.attr("r", function(d) { return 2.0* d["rad"]; })
.attr("id", function(d) { return "circle_"+d["ix"]; })
.style("fill", function(d) { return d["fill_color"]; })
AFTER ENTER
Now, if the user performs a click in one specific one of them, I change the radius of that one to 1.5*70 and call draw_circles again. This generates a new set of data so the update part will be called.
d3.select('#'+c_id)
.transition()
.duration(duration_till_next)
.attr("r", function(d) { return 1.50* d["rad"]; })
After this I get:
AFTER CLICK
This works as expected. Now, I have a setTimeout and after a couple of seconds, draw_circles gets called again from within itself, getting new data and triggering the update part of the d3 code above.
BUT! on the update part of the code, I change the radius to 140px, as you can see on the line above .attr("r", function(d) { return 2.0* d["rad"]; })
setTimeout(function(){ draw_circles(...);}, 2000);
AFTER UPDATE:
So as you can see, there is one element for which the radius update was not applied. However, all the other properties of the circle were changed, such as the coordinates in the image, the text inside of it or the fill color. Only the radius change is not observed.
Now it cannot be a coincidence that the property I happen to modify ad hoc is the very same one that doesn't get updated, but I cannot understand why.
Any ideas?
EDIT: Ok so the issue is here:
d3.select('#'+c_id)
.transition()
.duration(duration_till_next)
.attr("r", function(d) { return 1.50* d["rad"]; })
setTimeout(function(){ draw_circles(...);}, 2000);
The issue is that duration_till_next is also 2000. So what seems to be happening is that by the time the circles are updated, the circle has not finished transitioning. I would have expected that changing the circle radius is finished first, since it's supposed to be called first.
This makes the problem disappear:
.duration(0.9 * duration_till_next)
but this doesn't
.duration(0.99 * duration_till_next)
So there seems to be some sort of race going on.

Related

D3 join() only renders 'update' while doesn't show 'enter' elements

I tried to update an array of circles from the static positions
to dynamic force layout positions.
I tried to apply different colors for the circles within 'update' range
and apply another color to the new circles added from the new data.
however, newly added data nodes are not rendered for some reason.
Can anyone help me?
My update pattern is as below.
1.Initial Data Binding
let svg = d3.select('.graph')
.attr('width',width)
.attr('height',height)
svg.selectAll('circles').data(randoms)
.join('circle')
.attr('cx',d=>Math.random()*600)
.attr('cy',d=>Math.random()*600)
.attr('r',5)
2.Update Data binding
let simulation = d3.forceSimulation(randoms2)
.force('x', d3.forceX().x(function(d,i) {
if(i%2==0){
return 10;
}
else{
return 20
}
}))
.force('collision', d3.forceCollide().radius(function(d) {
return 1
}))
.on('tick',ticked)
function ticked(){
d3.selectAll('circle')
.data(randoms2)
.join( enter =>enter.append('circle')
.attr("fill", "green")
.attr('cx', function(d) {return d.x})
.attr('cy', function(d) {return d.y})
.attr('r',5),
update => update
.attr("fill", "red"),
exit => exit.remove()
)
.transition().duration(100)
.attr('cx', function(d) {return d.x})
.attr('cy', function(d) {return d.y})
}
It only shows the updated circles.
the complete code is in the following link.
https://codepen.io/jotnajoa/pen/xxEYaYV?editors=0001

d3 v5: How to transition only when a specific attribute changed?

I have a map with a circle at the center of each country. I apply an animation that updates the circles every 3 seconds, following the general update pattern by Mike Bostock. However, my problem is that I want to apply a transition to old elements that stay in the chart (i.e. are not part of the exit()), but only for those that have a change in a particular attribute (because this attribute defines the color of the circle). I figured that I should store the old value as an attribute in the circle and then just compare this attribute value with the newly assigned value from the updated data.
However, when I use a usual js if clause it only provides me with the first element, which means when this circle's value changes, all other circles will get the transition as well.
How can I check between the old and the new value inside this pattern?
Here is a sample code:
//DATA JOIN
var countryCircles = circleCont.selectAll(".dataCircle")
.data(filData, function(d){ return d.id})
//EXIT OLD CIRCLES THAT ARE NOT INCLUDED IN NEW DATASET
countryCircles.exit()
.transition()
.ease(d3.easeLinear)
.duration(500)
.attr("r",0)
.remove()
//CHECK IF OLD CIRCLES THAT ARE INCLUDED CHANGED THE CRITICAL ATTRIBUTE VALUE (main)
if (countryCircles.attr('main') != countryCircles.data()[0].main) {
countryCircles
.attr("main", function(d) {return d.main})
.attr("id", function(d) {return "circle" + d.id})
.attr("class","dataCircle")
.transition()
.ease(d3.easeLinear)
.duration(500)
.ease(d3.easeLinear)
.attr("r",0)
.transition()
.duration(500)
.ease(d3.easeLinear)
.attr("r",10)
.style("fill", function(d) {
if (d.main === "nodata") {
return "grey"
} else {
return color_data.find(function (y) {
return y.main === d.main;
}).color;
}
})
} else {
countryCircles
.attr("main", function(d) {return d.main})
.attr("id", function(d) {return "circle" + d.id})
.attr("class","dataCircle")
}
//CREATE CIRCLES THAT ARRE NEW IN THE UPDATED DATASET
var newCircle = countryCircles.enter()
.append("circle")
.attr("class","dataCircle")
.attr("cx", getCX)
.attr("cy", getCY)
.attr("id", function(d) {return "circle" + d.id})
.attr("main", function(d) {return d.main})
.style("cursor","crosshair")
.attr("r", 0)
.transition()
.delay(500)
.duration(500)
.ease(d3.easeLinear)
.attr("r",10)
.style("fill", function(d) {
if (d.main === "nodata") {
return "grey"
} else {
return color_data.find(function (y) {
return y.main === d.main;
}).color;
}
})
OK, the solution was somewhat straight forward (and I was blind...).
Instead of having the if statement for a single element, I use the each() function and within there check for a change.
...
//CHECK IF OLD CIRCLES THAT ARE INCLUDED CHANGED THE CRITICAL ATTRIBUTE VALUE (main)
countryCircles.each(function(d) {
if (d3.select(this).attr('main') != d3.select(this).data()[0].main) {
...

Circles on top of bar chart d3.js

I am making grouped bar chart based on Mike Bostock's tutorial.
I can't figure out how to put circles on top of my bars to act as tooltip when hovering, just like in this tutorial except it's on bars and not on a line.
I tried appending the circles like this :
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) { return x1(d.name); })
.attr("cy", function(d) { return y(d.value); })
});
But I get NaN values. I am very confused about which variable I should use to get the right cx and cy.
Here is my code.
Any ideas ?
Thank you
You will get NaN values since your data join is not correct, you are trying to get values that are not currently present in your data. In order to get those values you would need to make a reference to data.years.
Here is my approach:
// Inheriting data from parent node and setting it up,
// add year to each object so we can make use for our
// mouse interactions.
year.selectAll('.gender-circles')
.data(function(data) {
return data.years.map(function(d) {
d.year = data.year;
return d;
})
})
.enter().append('circle')
.attr("class", function(d) {
return "gender-circles gender-circles-" + d.year;
})
.attr("r", 10)
.attr('cx', function(d) {
console.log(d)
return x1(d.name) + 6.5;
})
.attr('cy', function(d) {
return y(d.value) - 15;
})
.style('display', 'none'); // default display
// ....
// Using an invisible rect for mouseover interactions
year.selectAll('.gender-rect-interaction')
.data(function(d) { // Inheriting data from parent node and setting it up
return [d];
})
.enter().append('rect')
.attr("width", x0.rangeBand()) // full width of x0 rangeband
.attr("x", function(d) {
return 0;
})
.attr("y", function(d) {
return 0;
})
.attr("height", function(d) { // full height
return height;
})
.style('opacity', 0) // invisible!
.on('mousemove', function(d) { // show all our circles by class
d3.selectAll('.gender-circles-' + d.year)
.style('display', 'block');
})
.on('mouseout', function(d) { // hide all our circles by class
d3.selectAll('.gender-circles-' + d.year)
.style('display', 'none');
});
Working plnkr: https://plnkr.co/edit/oH4KXdxdIW82nLGv46NI?p=preview

Old scaled data fail to disappear after axis rescale

Using D3 ver 3.5.5. I am using an example (https://gist.github.com/stepheneb/1182434) as a template: the example code to draw the data looks like this:
var circle = this.vis.select("svg").selectAll("circle")
.data(this.points, function(d) { return d; });
circle.enter().append("circle")
.attr("class", function(d) { return d === self.selected ? "selected" : null; })
.attr("cx", function(d) { return self.x(d.x); })
.attr("cy", function(d) { return self.y(d.y); })
.attr("r", 10.0)
.style("cursor", "ns-resize")
.on("mousedown.drag", self.datapoint_drag())
.on("touchstart.drag", self.datapoint_drag());
circle
.attr("class", function(d) { return d === self.selected ? "selected" : null; })
.attr("cx", function(d) {
return self.x(d.x); })
.attr("cy", function(d) { return self.y(d.y); });
circle.exit().remove();
I think of this as four sections: the first does selectAll("circles") and adds the data. The second tells where the data points are ("cx", "cy") and other attr(), and the third is a bit of mystery to me, because it appears to also set "cx" and "cy", but no other attributes. Finally, we do and exit().remove(), which the documentation says removes any data elements not associated with the data array. I dont see how this is happening in this example. When I set breakpoints into the code, both the "cx" steps get called for each data point in the this.points array.
In my code, I try to do the same steps:
hr_circles = self.graph_gps.svg.selectAll("hr_circles")
.data(self.graph_gps.datay1); // , function(d){return d;}
hr_circles.enter().append("circle")
.style("z-index", 3)
.attr("class", "y1")
.attr("r", 1)
.attr("cx", function (d, i) {
return xScale(d.time)
})
.attr("cy", function (d, i) {
return yScale(d.vy)
})
.on("mouseover",
function (d) {...displays a tooltip...})
.on("mouseout", function (d) {
});
hr_circles.attr("class", "y1")
.attr("cx", function (d, i) {
return xScale(d.time)
})
.attr("cy", function (d, i) {
return yScale(d.vy)
})
hr_circles.exit().remove();
When my graph initially displays, the data appear just fine, properly scaled, etc. When I try to re-scale by dragging on the x-axis (as in the example), the axis rescales itself just fine, and re-scaled data appears on the graph, but the original data is also still there (no longer scaled correctly), making a big mess! How do you erase or make the originally scaled data go away?
Tried to post images, but I guess my reputation is too low. Will send to anyone interested.

D3 Multiline Chart with Tooltip Transition Issue

I have been using d3 to create a multiline chart with focus and context brushing. Everything is going well except on the transition the dots at the data points with the tooltips are moving to a completely wrong position. I can't figure out what is causing this. Any help would be much appreciated. I attached the full code here and noted on the graph where I'm pretty sure the bug should be:
http://jsbin.com/osumaq/20/edit
When the button is clicked, a new json is passed to the graph to read.
The buggy block of code I think is this:
topicEnter.append("g").selectAll(".dot")
.data(function (d) { return d.values }).enter().append("circle").attr("clip-path", "url(#clip)")
.attr("stroke", function (d) {
return color(this.parentNode.__data__.name)
})
.attr("cx", function (d) {
return x(d.date);
})
.attr("cy", function (d) {
return y(d.probability);
})
.attr("r", 5)
.attr("fill", "white").attr("fill-opacity", .5)
.attr("stroke-width", 2).on("mouseover", function (d) {
div.transition().duration(100).style("opacity", .9);
div.html(this.parentNode.__data__.name + "<br/>" + d.probability).style("left", (d3.event.pageX) + "px").style("top", (d3.event.pageY - 28) + "px").attr('r', 8);
d3.select(this).attr('r', 8)
}).on("mouseout", function (d) {
div.transition().duration(100).style("opacity", 0)
d3.select(this).attr('r', 5);
});
Thank you very much.
What do you mean by tooltip ? Is it the window that appears when we hover on dots ? They seem fine. What I can see is that your dots are not moving while the lines are, and if I had to guess I would say your enter and update selections are mixed. If the dots are already on screen and you want to update their position (by calling your method update) you should have somthing along these lines :
// Bind your data
topicEnter.append("g").selectAll(".dot")
.data(function (d) { return d.values })
// Enter selection
topicEnter.enter().append("circle").attr("clip-path", "url(#clip)").attr("class", "dot");
// Update all the dots
topicEnter.attr("stroke", function (d) {
return color(this.parentNode.__data__.name)
})
.attr("cx", function (d) {
return x(d.date);
})
.attr("cy", function (d) {
return y(d.probability);
})
[...]

Categories