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
Related
I have a horizontal bar chart that gets updated when one of the radio buttons clicked. The bars get updated fine, however, the old labels seem not to be removed every time the labels get updated. Am I missing something here? it seems that the exit function is not working. I couldn't find examples that deal with labels.
svg_bar.selectAll(".text-bar")
.data(dataSet)
.join(
enter => enter
.append("text")
.attr('text-anchor', 'middle')
.attr('font-size', '16px')
.attr('font-family', 'sans-serif')
.attr('fill', 'white')
.call(enter => enter.transition()
.duration(1000)
.attr('y', (d) => yScale_h(d.clean_test) + yScale_h.bandwidth() / 2)
.attr('x', (d) => xScale_h(d.Award) - 14)
.text(function (d) {
return `${d.Award} `;
})
),
update => update
.call(update => update.transition()
.duration(1000)
.text(function (d) {
return `${d.Award} `;
})
.attr('y', (d) => yScale_h(d.clean_test) + yScale_h.bandwidth() / 2)
.attr('x', (d) => xScale_h(d.Award) - 14)
),
exit => exit
.call(exit => exit.transition()
.duration(1000)
.remove()
)
);
If you use svg_bar.selectAll(".text-bar"), the selection is always empty and d3 will always add new elements.
If you use svg_bar.selectAll("text"), the selection will include all text elements of the svg, and it will change other text elements such as the y-axis and title.
One approach to select only the texts of the bars is to select with a class, such as svg_bar.selectAll("text.bar"). For this to work, the appended bars need to be assigned to the 'bar' class with .classed('bar', true), so they selected in the next render.
svg_bar.selectAll("text.bar") // Selects the texts of class "bar"
.data(dataSet)
.join(
enter => enter
.append("text")
.classed("bar", true) // Create texts with class "bar"
.attr('text-anchor', 'middle')
...
i've stuck for this code. Im trying to add text behind the circle
and sample code like this
for the text:
g.selectAll(".my-text")
.data(marks)
.enter().append("text")
.attr("class", "text-desc")
.attr("x", function(d, i) {
return projection([d.long, d.lat])[0];
})
.attr("y", function(d, i) {
return projection([d.long, d.lat])[1];
})
.attr('dy', ".3em")
.text(function() { return location})
.attr("text-anchor", "middle")
.attr('color', 'white')
.attr('font-size', 15)
and for circle like this
g.selectAll(".circle")
.data(marktests)
.enter().append("circle")
.attr("class", "bubble")
.attr("cx", function(d, i) {
return projection([d.long, d.lat])[0];
})
.attr("cy", function(d, i) {
return projection([d.long, d.lat])[1];
})
.attr("r", function() {
return myRadius(locationGroup + 20);
})
.on('mouseover', tipBranch.show)
.on('mouseout', tipBranch.hide)
.on('click', function(d){
window.open('http://localhost:8000/detail/'+d.branch);
});
}
but i got result just like this
and the elements if using inspect element
Thank you if you can help to help me and explain how to solve the problem code
First of all I noticed the following issue:
g.selectAll(".my-text")
.data(marks)
.enter().append("text")
.attr("class", "text-desc")
Also the following line: .text(function() { return location}) is faulty because you are missing the data object that you iterate with. This might be changed to: .text(function(d) { return d.location})
you are selecting all elements with class .my-text but then you are attaching text-desc as class to the text elements. The correct change for this would be:
g.selectAll(".text-desc")
.data(marks)
.enter().append("text")
.attr("class", "text-desc")
considering that you want to use text-desc as a class. the same problem is with the circle as well: Either do: g.selectAll("circle") to select the circle tag elements or g.selectAll(".bubble") to select the bubbles.
You are also using different iterating objects for text and circles - usually you should iterate over a single collection.
Another issue with the sample is that location and locationGroup are not part of the collection items. I would expect that the values to be taken from the data object as such .text( d => d.location) and .attr("r", d => myRadius(d.locationGroup)). Before proceeding make sure that you populate iterating items with this properties.
Another approach would be to do the following:
const group =
g.selectAll('.mark')
.data(marks)
.enter()
.append('g')
.attr('class', 'mark')
.attr('transform', d => {
const proj = projection([d.long, d.lat])
return `translate(${proj[0]}, ${proj[1]})`;
})
group.append('text').text(d => return d.location) //apply other props to text
group.append('circle').text(d => return d.location) //apply other props to circle
Using this approach will allow you to iterate the collection with a group element and use translation property in order to move the group to the location (small improvement, projection will be executed once) and use the group to populate with other elements: text, circle.
Hope it helps.
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.
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.
//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.