D3 Data updates not working correctly - d3.js

I thought I understood the D3 enter/update/exit, but I'm having issues implementing an example. The code in http://jsfiddle.net/eamonnmag/47TtN/ illustrates what I'm doing. As you'll see, at each 5 second interval, I increase the rating of an item, and update the display again. This works, in some way. The issue is in only updating what has changed - D3 is updating everything in this case. The enter and exit methods, displayed in the console output that nothing has changed, which makes my think that it's treating each array as a completely new instance.
My understanding of the selectAll() and data() calls was that it would 'bind' all data to a map called 'chocolates' somewhere behind the scenes, then do some logic to detect what was different.
var chocolate = svg.selectAll("chocolates").data(data);
In this case, that is not what's happening. This is the update code. Any pointers to what I've missed are most appreciated!
function update(data){
var chocolate = svg.selectAll("chocolates").data(data);
var chocolateEnter = chocolate.enter().append("g").attr("class", "node");
chocolateEnter.append("circle")
.attr("r", 5)
.attr("class","dot")
.attr("cx", function(d) {return x(d.price)})
.attr("cy", function(d) {
//put the item off screen, to the bottom. The data item will slide up.
return height+100;})
.style("fill", function(d){ return colors(d.manufacturer); });
chocolateEnter
.append("text")
.text(function(d) {
return d.name;})
.attr("x", function(d) {return x(d.price) -10})
.attr("y", function(d){return y(d.rating+step)-10});
chocolateEnter.on("mouseover", function(d) {
d3.select(this).style("opacity", 1);
}).on("mouseout", function(d) {
d3.select(this).style("opacity", .7);
})
chocolate.selectAll('circle')
.transition().duration(500)
.attr('cy', function(d) {return y(d.rating+step)});
var chocolateExit = chocolate.exit().remove();
chocolateExit.selectAll('circle')
.attr('r', 0);
}
setInterval(function() {
chocolates[3].rating = Math.min(chocolates[3].rating+1, 5);
update(chocolates);
}, 5000);

Easy as apple pie!
Why are you doing svg.selectAll("chocolates")? There is no HTML element in your DOM called chocolates.
You need to change that to svg.selectAll(".node"). That will fix the problem.

There are a couple of issues in your code. First, the logic to detect what's different is, by default, to use the index of the item. That is, the first data item is matched to the first DOM element, and so on. This works in your case, but will break if you ever pass in partial data. I would suggest using the second argument to .data() to tell it how to match:
var chocolate = svg.selectAll("g.node").data(data, function(d) { return d.name; });
Second, as the other poster has pointed out, selecting "chocolate" will select nothing, as there are no such DOM elements. Just select the actual elements instead.
Finally, since you're adding g elements for the data items, you might as well use them. What I mean is that currently, you're treating the circles and text separately and have to update both of them. You can however just put everything underneath g elements. Then you have to update only those, which simplifies your code.
I've made all the above changes in your modified fiddle here.

Related

How to add label or custom value on Mapchart's path using geoChoroplethChart and dc.js?

var IndChart = dc.geoChoroplethChart("#india-chart");
var states = data.dimension(function (d) {
return d["state_name"];
});
var stateRaisedSum = states.group().reduceSum(function (d) {
return d["popolation"];
});
IndChart
.width(700)
.height(500)
.dimension(states)
.group(stateRaisedSum)
.colors(d3.scale.ordinal().domain().range(["#27AE60", "#F1C40F", "#F39C12","#CB4335"]))
.overlayGeoJson(statesJson.features, "state", function (d) { //console.log(d.properties.name);
return d.id;
})
.projection(d3.geo.mercator().center([95, 22]).scale(940))
.renderLabel(true)
.title(function (d) { console.log(d); return d.key + " : " + d.value ;
})
.label(function (d) { console.log(d);}) ;
wanted to add Label or custom value(25%, added in Map chart screen-shots) in map chart for each path using dc.js.
In the comments above, you found or created a working example that answers your original question. Then you asked how to make it work for two charts on the same page.
This is just a matter of getting the selectors right, and also understanding how dc.js renders and redraws work.
First off, that example does
var labelG = d3.select("svg")
which will always select the first svg element on the page. You could fix this by making the selector more specific, i.e. #us-chart svg and #us-chart2 svg, but I prefer to use the chart.select() function, which selects within the DOM tree of the specific chart.
Next, it's important to remember that when you render a chart, it will remove everything and start from scratch. This example calls dc.renderAll() twice, so any modifications made to the first chart will be lost on the second render.
In contrast, a redraw happens when any filter is changed, and it incrementally changes the chart, keeping the previous content.
I prefer to listen to dc.js chart events and make my modifications then. That way, every time the chart is rendered or redrawn, modifications can be made.
In particular, I try to use the pretransition event whenever possible for modifying charts. This happens right after drawing, so you have a chance to change things without any glitches or pauses.
Always add event listeners before rendering the chart.
Adding (the same) handler for both charts and then rendering, looks like this:
usChart.on('pretransition', function(chart) {
var project = d3.geo.albersUsa();
var labelG = chart.select("svg")
.selectAll('g.Title')
.data([0])
.enter()
.append("svg:g")
.attr("id", "labelG")
.attr("class", "Title");
labelG.selectAll("text")
.data(labels.features)
.enter().append("svg:text")
.text(function(d){return d.properties.name;})
.attr("x", function(d){return project(d.geometry.coordinates)[0];})
.attr("y", function(d){return project(d.geometry.coordinates)[1];})
.attr("dx", "-1em");
});
usChart2.on('pretransition', function(chart) {
var project = d3.geo.albersUsa();
var labelG = chart.select("svg")
.selectAll('g.Title')
.data([0])
.enter()
.append("svg:g")
.attr("id", "labelG")
.attr("class", "Title");
labelG.selectAll("text")
.data(labels.features)
.enter().append("svg:text")
.text(function(d){return d.properties.name;})
.attr("x", function(d){return project(d.geometry.coordinates)[0];})
.attr("y", function(d){return project(d.geometry.coordinates)[1];})
.attr("dx", "-1em");
});
dc.renderAll();
I used one more trick there: since pretransition happens for both renders and redraws, but we only want to add these labels once, I use this pattern:
.selectAll('g.Title')
.data([0])
.enter()
.append("svg:g")
.attr("class", "Title");
This is the simplest data binding there is: it says we want one g.Title and its data is just the value 0. Since we give the g element the Title class, this ensures that we'll add this element just once.
Finally, the result of this expression is an enter selection, so we will only add text elements when the Title layer is new.
Fork of your fiddle.

D3js loop through variable length series for scatter plot

I have data that can have a variable numbers of series. And inside each of those series is a date and number that I want to plot as a scatter plot in D3js.
This is my (non working) code. It works when I do it straight, but not once I add the $.each loop. I'm pretty sure its some sort of problem with indexing or something like that.
var color = d3.scale.category20();
// Now actually add the data to the graph
$.each(mpgData, function(k, v) {
console.log(v);
//console.log(k);
svg.selectAll('circle')
.data(v)
.enter()
.append('circle')
.attr('cx', function(d, i) {
console.log(i);
//console.log(d);
return xScale(getDate(d[1]));
})
.attr('cy', function(dd, ii) {
//console.log(ii);
return yScale(dd[2]);
})
.attr('fill', function(d, i) {
return color(k);
})
.attr("class", "mpgColorClass"+k)
.attr("r", 5)
.on("mouseover", function() {
d3.selectAll(".mpgColorClass"+k)
.attr("r", 8);
})
.on("mouseout", function() {
d3.selectAll(".mpgColorClass"+k)
.attr("r", 5);
});
});
I only showed what I think is the relevant part.
So that code kind of works. But it only shows 6 things, which I think is because the 2nd 'series' has 6 items. So somehow its not looping over everything at the "attr('cx', function(d, i)) part. I think I'm not understanding how to get that function to loop over each part of the series.
I'm new to D3js, so still struggling through the learning curve. But it works and I get a graph out with the correct data. Its just not ALL the data, only 6 points out of the entire (variable) dataset.
Thanks!
in your $.each() block you are overwriting the same set of circles in the SVG element. So instead of using selectAll('circle') you can do this:
$.each(mpgData, function(k, v) {
svg.selectAll('circle' + k)
.data(v)
.enter()
.append('circle')
.attr('class','circle' + k)
});
truncated rest of details in your code... edit at will.

D3 force layout: individually positioning first and last node

I would like to create a static force-directed layout as the one in this example: http://bl.ocks.org/mbostock/1667139, but with the first and last node anchored on each side of the window (contrary to the example, my graph is not circular). This way I could clearly see where the graph starts and ends and also hopefully be able to give it a more linear conformation.
So far I have attempted to assign different classes to these two nodes and then give those classes fixed coordinates, like:
svg.select(".first")
.attr("cx", function(d) { return 10; })
.attr("cy", function(d) { return height/2; })
svg.select(".last")
.attr("cx", function(d) { return width-10; })
.attr("cy", function(d) { return height/2; })
But it doesn't seem to be working, the two nodes are just sitting in the upper right corner of the window. Any ideas?
Placing the following code between the for loop and force.stop() seemed to work fine for me.
var nodes = force.nodes();
nodes[0].x = 10;
nodes[0].y = height/2;
nodes[nodes.length-1].x = width-10;
nodes[nodes.length-1].y = height/2;
Here's a Fiddle showing this. You might also want to look into the fixed property, which could help with the behaviour you're looking for.

D3.js nested data - how can i display my circles

I am a beginner on D3.js and hit the wall on the following:
I wish to display the score given by an attendee of an event over time. Then as the attendee can give also comments, I would like to place a circle on the curve of the score in the same colour as the scoring.
I succeeded to do this for a single user.
The code is on JS Fiddle http://jsfiddle.net/roestigraben/8s1t8hb3/
Then, trying to extend this to multiple attendees, I run into problems.
The JSFiddle http://jsfiddle.net/roestigraben/Lk2kf1gh/
This code displays nicely the score data for the 3 attendees simulated. However the circles to display the possible comments (there is only one in the data set) from the attendees do not work
I try to filter the attendees array
svg.selectAll("circle")
.data(data.filter(function(d, i){ if(d.comment){return d}; })) // condition here
.enter().append("circle")
.attr("class", "dotLarge")
.attr({r: 5})
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.status); })
I think I need to go deeper into the nesting, but ....my ignorance.
Thanks a lot
Peter
The code where you're displaying your circles doesn't even come close to matching the format of your data. I don't know that you're having a problem with the nest, but you probably want a slightly different data structure when it comes to graphing your comments.
I have updated your fiddle here: http://jsfiddle.net/Lk2kf1gh/7/
The important bit is:
var comments = attendees.map(function(d) {
return {
name: d.name,
comments: d.values.filter(function(e) {
return e.comment != undefined;
})
};
});
//generation of the circles to indicate a comment
// needs to be done with a filter
svg.selectAll("g.comments")
.data(comments)
.enter()
.append("g")
.attr("class", function(d) { return "comments " + d.name })
.selectAll("circle.comment")
.data(function(d) {
return d.comments;
})
.enter()
.append("circle")
.attr("class", "dotLarge comment")
.attr("r", 5)
.attr("cx", function(e) { return x(e.time); })
.attr("cy", function(e) { return y(e.status); });
The first part of this code creates a new data structure that is more focused on the comment information. The second part creates the circles.
Note: I've grouped each person into their own g element and then created a circle for each of their comments. This makes use of d3 nested selections and data binding. Hopefully you can follow what is happening.
I've also added a few more comments to your data for testing. I didn't bother to fix any of the cosmetic issues, I'll leave that up to you.

Update Selection in D3 is empty

In my code, my update selection is always empty, as is my exit selection, so the transitions never run. Every time I refresh, I end up redrawing the entire DOM fragment as if it never existed before (i.e. I can remove everything but .enter and the behavior doesn't change).
I'm making use of a key function in data() to ensure the join is being made on a unique value instead of by position.
The entire code is at http://jsfiddle.net/colin_young/xRQjX/23/, but I've extracted what I think is the relevant section here (basically, I'm just trying to follow the General Update Pattern):
var key = function (d) {
return d.index;
}
var filterDistance = function () {
var names = list.selectAll("div")
.data(byDistance.bottom(40), key);
// Update
names.attr("class", "update");
// Add
names.enter()
.append("div")
.attr("class", "enter")
.style("opacity", "0")
.transition()
.duration(500)
.style("opacity", "1")
.text(function (d) {
return displayText(d);
});
// Remove
names.exit()
.transition()
.duration(750)
.style("opacity", "0")
.remove();
};
bewest at https://groups.google.com/forum/?fromgroups=#!topic/d3-js/9mrspdWqkiU was able to find the problem. It turned out that in my "working" summary display, I was using $('results').text('insert summary here') which of course wipes out anything else that I might have stuck in there (like my D3 generated divs for instance).

Resources