today I created a simple Bar Chart with the enter-update-exit logic - everything works fine. Now I will do the same with a line chart - the whole dataset for the chart can change at any time, so I will update the chart smoothly. I can't find a good example with a line chart and the enter-update-exit logic (or I'm looking wrong). Currently I have to remember that if the chart is called for the first time or the data to be updated (data has changed) - This is my dirty version:
var channelGroup = d3.select(".ch1");
// init. Line Chart
if (firstLoad) {
channelGroup
.append('path')
.attr("class", "line")
.style("stroke", channel.Color)
.transition()
.ease("linear")
.duration(animChart / 2)
.attr('d', line(channel.DataRows));
}
// update Line Chart
else {
channelGroup
.selectAll("path")
.data([channel.DataRows])
.transition()
.ease("linear")
.duration(animChart / 2)
.attr("d", function (d) { return line(d); });
}
how can I realize this in a good manner?... thx!
You're mixing up two different approaches, one binding data to the line and the other just passing the data to the line function. Either one could work (so long as you only have the one line), but if you want to get rid of your if/else construct you're still going to need to separate the statements handling entering/appending the element from the statements updating it.
Option 1 (don't bind data, just call the function):
var linegraph = channelGroup.select('path');
if ( linegraph.empty() ) {
//handle the entering data
linegraph = channelGroup.append('path')
.attr("class", "line")
.style("stroke", channel.Color);
}
linegraph
.transition()
.ease("linear")
.duration(animChart / 2)
.attr('d', line(channel.DataRows));
Option 2 (use a data join):
var linegraph = channelGroup.selectAll("path")
.data([channel.DataRows]);
linegraph.enter().append('path')
.attr("class", "line");
linegraph
.transition()
.ease("linear")
.duration(animChart / 2)
.attr("d", line); //This is slightly more efficient than
//function (d) { return line(d); }
//and does the exact same thing.
//d3 sees that `line` is a function, and
//therefore calls it with the data as the
//first parameter.
Related
I am creating a line chart with D3.js. The line should appear over time while new data points are calculated 'on the fly' (i.e. the data array constantly grows). I have the function that does the data calculation, as well as the updateLine() function you see below, in a setInterval(). My problem is, that this function creates a new svg path for every newly added data point, resulting in a huge number of <path> elements.
function updateLine() {
canvas
.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
)
}
How can I 'extend' the existing path with the new data points?
I found an answer:
Clearly, the code above appends a new <path> every time the function is called. To avoid a <path> 'flood' in the DOM, I found two options:
Option 1
Before appending a new path, select the old path, that lacked the new data points, and remove it by calling remove(). In order to avoid selecting the wrong path, I used an ID selector.
function updateLine() {
canvas
.selectAll('#myPath').remove();
canvas
.append('path')
.datum(data)
.attr('id', '#myPath')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
)
}
Option 2 (more elegant, I think)
Use a selectAll() to select the path, bind the data to the selection and call a join() on the selection. In the first call of updateLine(), the path is created (no SVG path exists and we have a nonempty enter selection to which a SVG path is appended and all attributes are set). In the following calls, the path exists in the DOM and the newly updated data array is bound to it. Thus the enter selection is empty and the update selection gets relevant, where we update the path with the new data.
function updateLine() {
canvas.selectAll('#myPath')
.data([data])
.join(
function(enter) {
console.log('Enter selection:');
console.log(enter);
return enter
.append('path')
.attr('id', 'myPath')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
);
},
function(update) {
console.log('Update selection:');
console.log(update);
return update
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
);
}
);
}
A couple notes regarding the code of option 2:
It is important to use a selectAll() and not just a select() here,
since in the first call of the function, no <path> exists. select() would select the first match which remains empty in this case.
I call data([data]) and thus perform a join of data points in the data array with SVG elements. datum() would, to my understanding, not perform a join, however, this is important here, as we rely on the update selection.
Passing the data array as an array again to data([data]) causes a data bind of all data points to the one path element, which is exactly what we want here.
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.
I have tried to create a map of India with some points in it. I followed the codebase from here.
Everything is fine except the points. They are hidden behind other features on the map, and because of this are not visible. How do I layer the features so that the points are visible?
In d3.js map layering can be handled in two ways. If this is your code (paraphrasing from your example)
d3.json("path.json",function (json) {
g.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("path.csv",function (csv) {
g.selectAll("circle")
.data(csv)
.enter().append("circle")
.attr("cx", function(d) { projection([d.x,d.y])[0] })
.attr("cy", function(d) { projection([d.x,d.y])[1] })
.attr("r",4);
});
Data will be added to the 'g' element based on the order in which the callback functions are completed, so it is possible that the csv data will be drawn first and the json data will be drawn after it.
The first method I'll present here is the cleanest way in most situations to specify data layer order (in my mind). SVG 'g' elements are appended in the order that they are specified. This gives you easy control over the layering of data:
var gBackground = svg.append("g"); // appended first
var gDataPoints = svg.append("g"); // appended second
// ... and so forth
Then, all you have to do is specify to which 'g' element/layer data gets appended/inserted into. So, your code would look more like:
d3.json("path.json",function (json) {
gBackground.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("path.csv",function (csv) {
gDataPoints.selectAll("circle")
.data(csv)
.enter().append("circle")
.attr("cx", function(d) { projection([d.x,d.y])[0] })
.attr("cy", function(d) { projection([d.x,d.y])[1] })
.attr("r",4);
});
The second option appends data to the same 'g' element but ensures the order in which this is done is controlled, by drawing the second layer in the callback function that draws the first, after the first is drawn:
To control the ordering of the data with this method we would modify the code to something like:
d3.json("path.json",function (json) {
g.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
// once the json is drawn, draw the csv:
d3.csv("path.csv",function (csv) {
g.selectAll("circle")
.data(csv)
.enter().append("circle")
.attr("cx", function(d) { projection([d.x,d.y])[0] })
.attr("cy", function(d) { projection([d.x,d.y])[1] })
.attr("r",4);
});
});
This question has been asked before but I don't think the given solution is the cleanest way to do it so I'm hoping someone may have figured it out since then. I am generating multiple pie charts using d3.js and am dynamically updating them through SQL queries. This is my update function:
function updateCharts()
{
var updatedDataSet = getDataSet();
// Create a pie layout and bind the new data to it
var layout = d3.layout.pie()
.value(function(d, i) { return d[i].count; })
.sort(null);
// Select the pie chart
var pieChartSVG = d3.selectAll("#pie");
// Select each slice of the pie chart
var arcsUpdate = pieChartSVG.selectAll("g.slice")
.data(layout([updatedDataSet]))
.enter();
// Apply transitions to the pie chart to reflect the new dataset
arcsUpdate.select("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.transition()
.duration(1000)
.attrTween("d", arcTween);
}
But it doesn't work. If I take the .enter() out of arcsUpdate then it works but applies the same changes(data and tweens) to each chart. I could get around this by doing a foreach() on the elements returned from pieChartSVG but I can't think of a way of doing that other than the one described in the other question.
I have had to use the solution from the other question as I have to move forward but it's not a "clean" solution so I'd love to know if anybody is aware of a better way to handle it.
I thought you need take the .enter() out of arcsUpdate just like
var arcsUpdate = pieChartSVG.selectAll("path")
.data(layout([updatedDataSet]));
// Apply transitions to the pie chart to reflect the new dataset
arcsUpdate.enter()
.append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.transition()
.duration(1000)
.attrTween("d", arcTween);
This is the correct way.
And if it applies the same changes(data and tweens) to each chart. Please check out they are binding same updateDataSet or not.
I'm building a ring chart on last.fm's API, using live data on music listening habits. I want users to be able to enter their username, and the number of most-popular artists they'd like to see. They can then enter another username or another number of artists, and the chart re-draws itself with that new pulled data.
However, right now, I'm seeing that when I pull data that has less data points than the previous set (e.g. I ask for the top 50 artists from user X, and then the top 3 artists from user X), the ring chart ends up being remade with gaps, like so:
And then:
Moving from smaller to larger datasets (e.g. 5 artists to 50 artists) doesn't generate this problem. Here's my transition code:
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
var sum = 0;
for (i=0 ; i<d.values.length ; i++) {
sum += d.values[i].playcount;
}
return sum;
});
var path = svg.selectAll("path")
.data(pie(dataset));
path.exit().remove();
path.enter()
.append("path")
.attr("fill", function(d, i) { return color(i); })
.style("stroke-width", "1px")
.style("stroke", "white")
.attr("d", arc);
I'm convinced it has something to do with the exit/update/enter order. When I check each element in the DOM, though, the data seems to be updating and attaching itself to the right elements. I don't understand why it's not filling up to a new full, 360-degree ring chart though. Full JSFiddle here.
It was a simple enough fix, in the end, arrived after some time meditating over Thinking with Joins:
Before (gives gap-toothed ring charts):
var path = svg.selectAll("path")
.data(pie(dataset));
path.exit().remove();
path.enter()
.append("path")
.style("stroke-width", "1px")
.style("stroke", "white")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
After (no gaps):
var path = svg.selectAll("path")
.data(pie(dataset));
path.exit().remove();
path.enter()
.append("path")
.style("stroke-width", "1px")
.style("stroke", "white");
path.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
Now the mystery: why do we need to restart path. when dealing with the d elements?
First time when you load the data with 10 items, D3JS bind these items' data to new 10 paths. In the next time, you load new data with 3 items, by default without using key function, first 3 items are updated because there has been 3 paths existed with its d attribute having old values; 7 previous paths are removed. You should add key function or update d attribute as following:
var path = svg.selectAll("path")
.data(pie(dataset));
path.attr("d", arc); // only need to add one line here, other lines aren't changed.
path.exit().remove();
path.enter()
.append("path")
.style("stroke-width", "1px")
.style("stroke", "white")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
Hey it's probably been a really long time but I figured out that whatever you don't separate from enter() and append() doesn't get updated. So if you for example change your stroke width or stroke color you need to move it to the second path. in order for them to get updated. Currently you only update the arc and the fill color.
I had this problem and I was tearing out my hair over it, so thanks for the help!