How to update selection with new data in D3? - javascript

I'm trying to edit the data of created circles in D3. Below my code is pasted of me creating a lot of circles based on some data from graphData.
Supposed I'd want to re-arrange my circles Y position with a new dataset, by transitioning them to their new destinations. How would perform this task? I've tried using attr.("cy", function(d){return yScale(parseFloat(d))} ) to update my Y-coordinates by adding data(graphData[i], function(d){return d;}) with my new data, but this does not work.
You can take a look at my JSFiddle: http://jsfiddle.net/RBr8h/1/
Instead of the for-loop in the following code I've created circles on 2 ticks of my X-axis. I have 3 sets of data and I've used to of them in the example in the fiddle. I'd like to able to use the 3rd dataset instead of the 2 first ones on both circles.
var circle;
for(var i = 0;i < graphData.length;i++){
circle = SVGbody
.selectAll("circle")
.data(graphData[i], function(d){return d;})
.enter()
.append("circle")
.attr("cx",xScale(0))
.attr("cy", yScale(minAxisY))
.attr("r",4)
.style('opacity', 0)
.transition()
.duration(1000)
.attr("cx", function(d){
return spreadCircles(i);
})
//.attr("cy", function (d, i){ return yScale(i); })
.style('opacity', 1)
.transition()
.duration(1500)
.attr("cy", function(d){return yScale(parseFloat(d))} );
Thank you for your help in advance!

To put some flesh on Lars comment, here is a FIDDLE leveraging the enter/update/exit pattern to help you out. I have altered and simplified your code (and data) just enough to demonstrate the principle.
function updateCircles(dataset,color) {
var circle = SVGbody
.selectAll("circle")
.data(dataset, function(d) { return d; });
circle
.exit()
.transition().duration(750)
.attr("r", 0)
.remove();
circle
.enter()
.append("circle");
circle
.attr("cx",function(d){return xScale(100);})
.attr("cy",function(d){return yScale(parseFloat(d))})
.attr("r",0)
.transition().duration(1500)
.attr("r",5)
.style("fill", color);
};
Update fiddle with data keyed off by index...so, circles just have their position updated.

Related

dot (symbol) color on d3.js multiline chart

I am trying to replicate this example of a multiline chart with dots. My data is basically the same, where I have an object with name and values in the first level, and then a couple of values in the second level inside values. For the most part, my code works, but for some reason, the j index in the anonymous function for the fill returns an array of repeated circle instead of returning the parent of the current element. I believe this may have something to do with the way I created the svg and selected the elements, but I can't figure it out. Below is an excerpt of my code that shows how I created the svg, the line path and the circles.
var svgb = d3.select("body")
.append("svg")
.attr("id","svg-b")
.attr("width", width)
.attr("height", height)
var gameb = svgb.selectAll(".gameb")
.data(games)
.enter()
.append("g")
.attr("class", "gameb");
gameb.append("path")
.attr("class", "line")
.attr("d", function(d) {return line_count(d.values); })
.style("stroke", function(d) { return color(d.name); })
.style("fill", "none");
gameb.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3)
.style("fill", function(d,i,j) {console.log(j)
return color(games[j].name);});
j (or more accurately, the third parameter) will always be the nodes in the selection (the array of circles here), not the parent. If you want the parent datum you can use:
.attr("fill", function() {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
return color(datum.name);
})
Note that using ()=> instead of function() will change the this context and the above will not work.
However, rather than coloring each circle independently, you could use a or the parent g to color the circles too:
gameb.append("g")
.style("fill", function(d) { return color(d.name); })
.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3);
Here we add an intermediate g (though we could use the original parent with a few additional modifications), apply a fill color to it, and then the parent g will color the children circles for us. The datum is passed on to this new g behind the scenes.

How can I use column values for SVG colors using JS/D3?

I searched for this and found a lot of info on how to set scales, but I'm trying to color individual SVGs created in D3 using values from a column that's populated with hex values. In the code below "Color1" is the column populated with different hex color values, e.g., #000000;
Here's what I've tried that makes intuitive sense to me but isn't working, instead the chart populates with the fill as black:
var circles = svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx',function (d) { return xScale(d.xvalue) })
.attr('cy',function (d) { return yScale(d.yvalue) })
.attr('r','3')
.attr('stroke','black')
.attr('stroke-width',1)
.attr('fill', function (d) {return d.Color1})
I've also tried surrounding the function with "'" but was unsuccessful.
The color property should not include a semicolon:
var data = [
{Color1: "#aaaaaa;"},
{Color1: "#aaaaaa"}
]
var svg = d3.select("body").append("svg");
var circles = svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx',(d,i)=>i*100+50)
.attr('cy', 100)
.attr('r','10')
.attr('stroke','black')
.attr('stroke-width',1)
.attr('fill', function(d) { return d.Color1; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
You could just slice the last character off if you have the semicolon hard coded in your data:
.attr("fill", function(d) { return d.Color1.slice(0,-1); })

D3 not updating label

I have a map and a matching legend on my website. As the user selects different values from a select list, the map is updated and in the same function, the legend should be updated with new values. As the map actualization works properly, the values of the legend stay the same even in the console are logged the right values if I log the variables.
This is the function that draws the legend:
color_domain = [wert1, wert2, wert3, wert4, wert5];
ext_color_domain = [0, wert1, wert2, wert3, wert4, wert5];
console.log(ext_color_domain);
legend_labels = ["< "+wert1, ""+wert1, ""+wert2, ""+wert3, ""+wert4, "> "+wert5];
color = d3.scale.threshold()
.domain(color_domain)
.range(["#85db46", "#ffe800", "#ffba00", "#ff7d73", "#ff4e40", "#ff1300"]);
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
var ls_w = 20, ls_h = 20;
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.append("text")
.attr("x", 50)
.attr("y", function(d, i){ return height - (i*ls_h) - ls_h - 4;})
.text(function(d, i){ return legend_labels[i]; });
console.log(legend_labels); //gives the right legend_labels but doesn't display them correctly
};
Sadly even the map is updated with new colors they're colored with the old thresholds. This is the way the map is colored:
svg.append("g")
.attr("class", "id")
.selectAll("path")
.data(topojson.feature(map, map.objects.immoscout).features)
.enter().append("path")
.attr("d", path)
.style("fill", function(d) {
return color(rateById[d.id]);
})
This is tough to answer without a complete, working code sample but...
You are not handling the enter, update, exit pattern correctly. You never really update existing elements, you are only re-binding data and entering new ones.
Say you've called your legend function once already, now you have new data and you do:
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
This re-binds the data and computes an enter selection. It says, hey d3, what data elements are new? For those new ones, you then append a g. Further:
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
Again, this is operating on those newly entered elements only. The ones that already existed on the page aren't touched at all.
Untested code, but hopefully it points you in the right direction:
// selection of all enter, update, exit
var legend = svg.selectAll("g.legend")
.data(ext_color_domain); //<-- a key function would be awesome here
legend.exit().remove(); //<-- did the data go away? remove the g bound to it
// ok, what data is coming in? create new elements;
var legendEnter = legend.enter().append("g")
.attr("class", "legend");
legendEnter.append("rect");
legendEnter.append("text");
// ok, now handle our updates...
legend.selectAll("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.selectall("text")
...
There's some really great tutorials on this; and it's confusing as hell, but it's the foundation of d3.
An example that helps you get started with updating d3 (d3, v4):
const line = svg.selectAll('line').data(d3Data); // binds things
line.exit().remove(); // removes old data
line.enter()
.append('line') // add new lines for new items on enter
.merge(line) // <--- this will make the updates to the lines
.attr('fill', 'none')
.attr('stroke', 'red');

d3 transition circles to a json data value

I am trying to set up a force directed layout in d3 and have my nodes start with an initial radius at 0. I then want for the user to be able to press a button and have the radius of the nodes scale to a size in accordance with a json data value. When I try to do this however, I get a javascript error "Cannot read property 'FIELD4' of undefined.
Here is the json data: https://api.myjson.com/bins/2n7do
And here is the code:
// Update nodes.
circles = circles.data(data);
circles.exit().remove();
var nodeEnter = circles.enter().append("g")
.attr("class", "node")
.style("fill", function(d) { return color(d.FIELD5); })
.style("opacity", 0.75)
.call(force.drag);
nodeEnter.append("circle")
.attr("r", 9)
d3.select(circles).transition()
.delay(3000)
.duration(1000)
.attr("r", function(d) { return d.FIELD4 * 0.000195 });;
circles is d3 selection. By doing d3.select(circles) you select selection. I don't think it's valid.
Instead of d3.select(circles).transition() try circles.transition()

Overlays not showing up on top of data-generated d3 shapes

I am a noob with d3.js. I am using topoJSON data to render maps and so far it's been working great. Now I want to overlay some data such as text or circles on top of each country/region and I am hitting a wall. I have code similar to this:
var countries = g.append("g")
.attr("id", "countries")
.selectAll("path")
.data(topojson.feature(collection, collection.objects.countries).features)
.enter().append("path")
.attr("d", path)
.style("fill", colorize)
.attr("class", "country")
.on("click", clicked)
which properly renders my map. In order to overlay some circles on it, I do the following:
countries
.append("circle")
.attr("r", function(d, i, j) {
return 10; // for now
})
// getCentroid below is a function that returns the
// center of the poligon/path bounding box
.attr("cy", function(d, i, j) { return getCentroid(countries[0][j])[0]})
.attr("cx", function(d, i, j) { return getCentroid(countries[0][j])[1]})
.style("fill", "red")
Which is pretty cumbersome (specially the way it accesses the countries array), but it succeeds at appending a circle for each path representing a country. The problem is that the circle exists in the SVG markup, but doesn't show up at all in the document. I am obviously doing something wrong, but I am at a loss of what is it.
The problem is that you're appending the circle elements to path elements, which you can't do in SVG. You need to append them to the parent g elements. The code would look something like this.
var countries = g.selectAll("g.countries")
.data(topojson.feature(collection, collection.objects.countries).features)
.enter().append("g")
.attr("id", "countries");
countries.append("path")
.datum(function(d) { return d; })
.attr("d", path)
// etc
countries.append("circles")
// etc

Categories

Resources