How to update line chart points when the source data changes - d3.js

I've created a multi-line chart from the following source data.
[
{
"key":"108",
"title":"Series 1",
"items":[
{
"key":"54048872e9c2021fd8231051",
"value":1.0
},
{
"key":"540488a1e9c2021fd823107b",
"value":2.0
}
]
},
{
"key":"15",
"title":"Series 2",
"items":[
{
"key":"54048872e9c2021fd8231051",
"value":1.0
},
{
"key":"540488a1e9c2021fd823107b",
"value":4.0
}
]
}
]
We render a line for each Series and circles for each data "point"
var series = svg.selectAll('.series')
.data(this.data)
.enter()
.append('g')
.attr('class', 'series');
series.append('path')
.attr('class', 'line');
// add points to chart
var points = series.append('g')
.attr('class', 'point')
.selectAll('circle')
.data(function (d) {
return d.items;
})
.enter()
.append('circle')
.attr('r', 5);
Then when we first render the chart or when the window resizes we actually set the coordinates of the line and circles:
this.svg.selectAll('.line')
.attr('d', function (d) {
return chart.line(d.items);
});
this.svg.selectAll('.point circle')
.attr('cx', function (d) { return chart.xScale(d.key); })
.attr('cy', function (d) { return chart.yScale(d.value); });
Here, chart.line corresponds to a d3 line generator previously created.
When the source data changes I can update the line by setting its data:
this.svg.selectAll('.line')
.data(this.data);
But I can't figure out how to do the same thing with the data points (circles).
I've tried the following but the data on the individual point is not actually updated. Here the first console log (after selecting the .point elements) returns the correct data but the cx attribute function is returning the old data.
var series = this.svg.selectAll('.series')
.data(this.data)
.selectAll('.point')
.data(function (d) {
console.log(d.items);
return d.items;
})
.selectAll('circle')
.attr('class', 'updated')
.attr('cx', function (d) { console.log(d); return chart.xScale(d.key); })
.attr('cy', function (d) { return chart.yScale(d.value); });

Related

d3.js multi bar does not refresh

My d3 multi bar does not refresh after data changes. If there is more bars it add it in the end of old but does not remove old ones. If there is less bars it does not add it at all. Axis changes all the time for that from new data
var bars = svg.select(".chart-group")
.selectAll(".state")
.data(data);
bars.enter().append("g")
.attr("class", "state")
.attr("transform", function (d) {
return "translate(" + x0Scale(d[KEY]) + ",0)";
});
bars.exit().remove();
bars.selectAll("rect")
.data(function (d) {
return d.ages;
})
.enter().append("rect")
.attr("width", x1Scale.rangeBand())
.attr("x", function (d) {
return x1Scale(d[SEGMENT]);
})
.attr("y", function (d) {
return yScale(d[DATA]);
})
.attr("height", function (d) {
return chartH - yScale(d[DATA]);
})
.style("fill", function (d) {
return color(d[SEGMENT]);
});
Edit:
Here is fiddle with my problem https://jsfiddle.net/wxw5zdws/
I achieve my goal when whole container is removed:
if (container) {
container.remove();
}
I think it is bad practice, there is an issue in my drawing bars method. It should use old elements and remove/add usseless/needed elements.
What is wrong with this bars?

Dynamically adjust bubble radius of counties?

Following the County Bubbles example, it's easy to add a bubble for each county. This is how it is added in the example:
svg.append("g")
.attr("class", "bubble")
.selectAll("circle")
.data(topojson.feature(us, us.objects.counties).features
.sort(function(a, b) { return b.properties.population - a.properties.population; }))
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("r", function(d) { return radius(d.properties.population); })
.append("title")
.text(function(d) {
return d.properties.name
+ "\nPopulation " + formatNumber(d.properties.population);
});
However, rather than using a variable from the json file (population), I need to update the radii according to a variable which dynamically changes (so I cannot put it in the json file beforehand as was done in the example). I call updateRadii() when a county is clicked, which needs access to the FIPS.
var currFIPS,
flowByFIPS;
var g = svg.append("g");
queue()
.defer(d3.json, "us.json")
.defer(d3.csv, "census.csv", function(d) {
return {
work: +d.workplace,
home: +d.residence,
flow: +d.flow
}
})
.await(ready);
function ready(error, us, commute) {
// Counties
g.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.on("click", function(d) {
// Get FIPS of selected county
currFIPS = d.id;
// Filter on selected county (i.e., grab
// people who work in the selected county)
var data = commute.filter(function(d) {
return d.work == currFIPS;
});
// Create d3.map for where these people live
flowByFIPS = d3.map(data, function(d) {
return d.home;
});
// Update radii at "home" counties to reflect flow
updateRadii();
});
// Bubbles
g.append("g")
.attr("class", "counties")
.selectAll("circle")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("circle")
.attr("id", function(d) { return d.id; })
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.attr("r", 0); // invisible before a county is clicked
}
function updateRadii() {
svg.selectAll(".counties circle")
.transition()
.duration(300)
.attr("r", function(d) {
return flowByFIPS.get(d.id).flow
});
}
According to the error code, I believe that the circles do not have an id (FIPS code) attached. How do I get them to have an id? (I tried nesting the circle with the path using .each as explained in this answer, but could not get it working.)
Note that the above code works for updating fill on paths (rather than circles). For example, sub updateRadii(); for updateFill(); with the function as:
function updateFill() {
svg.selectAll(".counties path")
.transition()
.duration(300)
.attr("fill", function(d) {
return flowByFIPS.get(d.id).color; // e.g., "#444"
});
}
The problem here is that you don't supply d3 with data in the update function. I will recommend you update the data loaded from the file on the clicks, and from there you update the svg.
var update = function() {
g.selectAll(".country")
.data(data)
.attr("r", function(d) { return d.properties.flow_pct });
};
var data = topojson.feature(us, us.objects.counties).features;
data.forEach(function(x) { x.properties.flow_pct = /* calc the value */; })
g.append("g")
.attr("class", "counties")
.selectAll(".country")
.data(data)
.enter()
.append("circle")
.attr("class", "country")
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.on("click", function(d) {
// more code
data.forEach(function(x) { x.properties.flow_pct = /* calc the NEW value */; })
update();
});
update();
I've tried to use as much as the same code as before, but still trying to straiten it a bit out. The flow is now more d3-like, since the update function works on the changed data.
Another plus which this approach is both first render and future updates use the same logic for finding the radius.
It turns out to be an obvious solution. I forgot to check for the cases where a flow doesn't exist. The code above works if updateRadii() is changed to:
function updateRadii() {
svg.selectAll(".counties circle")
.transition()
.duration(300)
.attr("r", function(d) {
if (currFIPS == d.id) {
return 0;
}
var county = flowByFIPS.get(d.id);
if (typeof county === "undefined") {
return 0;
} else {
return county.flow;
}
});
}

d3 join on object key within array

My data looks like this:
[
{
name: "Joel Spolsky",
values: [
{
timestamp: 1380432214730,
value: 55
},
{
timestamp: 1380432215730,
value: 32
},
{
timestamp: 1380432216730,
value: 2
},
{
timestamp: 1380432217730,
value: 37
},
// etc
]
},
{
name: "Soul Jalopy",
values: [
{
timestamp: 1380432214730,
value: 35
},
{
timestamp: 1380432215730,
value: 72
},
{
timestamp: 1380432216730,
value: 23
},
{
timestamp: 1380432217730,
value: 3
},
// etc
]
},
// and so on
]
I pass this data into d3.layout.stack so a y and y0 get added. I then draw this stacked layout.
When the data changes, I join the new data to the old.
I can join the groups on name like this:
var nameGroups = this.chartBody.selectAll(".nameGroup")
.data(this.layers, function (d) {
return d.name;
});
But I'm having trouble joining the rectangles (or "bars") on timestamp. The best I can do (so far) is join them on the values key:
var rects = nameGroups.selectAll("rect")
.data(function (d) {
return d.values;
});
How do I join this "inner data" on the timestamp key?
I've tried including the array index:
var rects = nameGroups.selectAll("rect")
.data(function (d, i) {
return d.values[i].timestamp;
});
But that doesn't work because (I think) the timestamp is matched per array index. That is, the join isn't looking at all timestamp values for a match, just the one at that index.
UPDATE
Here is my complete update function:
updateChart: function (data) {
var that = this,
histogramContainer = d3.select(".histogram-container"),
histogramContainerWidth = parseInt(histogramContainer.style('width'), 10),
histogramContainerHeight = parseInt(histogramContainer.style('height'), 10),
width = histogramContainerWidth,
height = histogramContainerHeight,
nameGroups, rects;
/*
FWIW, here's my stack function created within my
init function:
this.stack = d3.layout.stack()
.values(function (d) { return d.values; })
.x(function (dd) { return dd.timestamp; })
.y(function (dd) { return dd.value; });
*/
// save the new data
this.layers = this.stack(data);
// join the new data to the old via the "name" key
nameGroups = this.chartBody.selectAll(".nameGroup")
.data(this.layers, function (d, i) {
return d.name;
});
// UPDATE
nameGroups.transition()
.duration(750);
// ENTER
nameGroups.enter().append("svg:g")
.attr("class", "nameGroup")
.style("fill", function(d,i) {
//console.log("entering a namegroup: ", d.name);
var color = (that.colors[d.name]) ?
that.colors[d.name].value :
Moonshadow.helpers.rw5(d.name);
return "#" + color;
});
// EXIT
nameGroups.exit()
.transition()
.duration(750)
.style("fill-opacity", 1e-6)
.remove();
rects = nameGroups.selectAll("rect")
.data(function (d) {
// I think that this is where the change needs to happen
return d.values;
});
// UPDATE
rects.transition()
.duration(750)
.attr("x", function (d) {
return that.xScale(d.timestamp);
})
.attr("y", function(d) {
return -that.yScale(d.y0) - that.yScale(d.y);
})
.attr("width", this.barWidth)
.attr("height", function(d) {
return +that.yScale(d.y);
});
// ENTER
rects.enter().append("svg:rect")
.attr("class", "stackedBar")
.attr("x", function (d) {
return that.xScale(d.timestamp); })
.attr("y", function (d) {
return -that.yScale(d.y0) - that.yScale(d.y); })
.attr("width", this.barWidth)
.attr("height",function (d) {
return +that.yScale(d.y); })
.style("fill-opacity", 1e-6)
.transition()
.duration(1250)
.style("fill-opacity", 1);
// EXIT
rects.exit()
.transition()
.duration(750)
.style("fill-opacity", 1e-6)
.transition()
.duration(750)
.remove();
}
You're not actually passing a key function in your code. The key function is the optional second argument to .data() (see the documentation). So in your case, the code should be
.data(function(d) { return d.values; },
function(d) { return d.timestamp; })
Here the first function tells D3 how to extract the values from the upper level of the nesting and the second how, for each item in the array extracted in the first argument, get the key.

nested circles in d3

Using d3.js, how would I modify the following code to add a nested, yellow-filled circle of radius "inner_radius" to each of the existing generated circles:
var circleData = [
{ "cx": 300, "cy": 100, "radius": 80, "inner_radius": 40},
{ "cx": 75, "cy": 85, "radius": 50, "inner_radius": 20}];
var svgContainer = d3.select("body").append("svg")
.attr("width",500)
.attr("height",500);
var circles = svgContainer.selectAll("circle")
.data(circleData)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) { return d.cx; })
.attr("cy", function (d) { return d.cy; })
.attr("r", function (d) { return d.radius; })
.style("fill", function (d) { return "red"; });
As imrane said in his comment, you will want to group the circles together in a g svg element. You can see the updated code here with relevant changes below.
var circles = svgContainer.selectAll("g")
.data(circleData)
.enter()
.append("g");
// Add outer circle.
circles.append("circle")
.attr("cx", function (d) { return d.cx; })
.attr("cy", function (d) { return d.cy; })
.attr("r", function (d) { return d.radius; })
.style("fill", "red");
// Add inner circle.
circles.append("circle")
.attr("cx", function (d) { return d.cx; })
.attr("cy", function (d) { return d.cy; })
.attr("r", function (d) { return d.inner_radius; })
.style("fill", "yellow");

change the style with 'selectAll' and 'select' problems

I am trying to change the style of some svg element.
when I do this:
var circleSelected = d3.select("#circleid_2");
circleSelected.style("fill", "purple");
circleSelected.style("stroke-width", 5);
circleSelected.style("stroke", "red");
the circle is changing its style.
BUT, when i do this:
var allCircles = d3.selectAll(".circle");
allCircles.forEach(function (circle) {
circle.style("fill", "green"); //function(d) { return getNodeColor(d); }
});
it does not work with the error: Object [object SVGCircleElement] has no method 'style'
and here is my 'circle' declaration (note: it has both class and id):
node.append("circle")
.attr("id", function (d) { return "circleid_" + d.id; })
.attr("class", "circle")
.attr("cx", function (d) { return 0; })
.attr("cy", function (d) { return 0; })
.attr("r", function (d) { return getNodeSize(d); })
.style("fill", function (d) { return getNodeColor(d); })
.style("stroke", function (d) { return getNodeStrokeColor(d); })
.style("stroke-width", function (d) { return getNodeStrokeWidth(d); });
What am I doing wrong here? Thanks for the help!
Try:
d3.selectAll("circle").style("fill", "green");
Or:
allCircles.style("fill", "PaleGoldenRod");
Explanation: d3.selectAll will return a selection, which can be acted upon using the functions described in this API: https://github.com/d3/d3/blob/master/API.md#selections-d3-selection
However, as soon as you do forEach, the internal variable returned each time as circle will be an actual DOM element - no longer a selection, and therefore no style function attached.

Resources