Data join in a line chart - d3.js

I'm very new to d3, so goal #1 was to show the chart. This works with the following code:
const line = d3.line()
.x(d => this.x(d.x))
.y(d => this.y(d.y));
console.log('data', JSON.stringify(data), 'color', color);
// data [{"x":"2017-07-01T04:00:00.000Z","y":81.2},{"x":"2017-08-01T04:00:00.000Z","y":79.6},{"x":"2017-09-01T04:00:00.000Z","y":79.4},{"x":"2017-10-01T04:00:00.000Z","y":80.6},{"x":"2017-11-01T04:00:00.000Z","y":80},{"x":"2017-12-01T05:00:00.000Z","y":76}] color blue
g.append('path')
.datum(data)
.attr('class', 'line')
.attr('stroke', color)
.attr('d', line);
With this code, anytime I run this method again, I get a new line, which is expected since I'm appending. I want to update only the stroke and d attributes when I have new data and/or color, I replace the code after the console.log with:
const lines = g.selectAll('.line').data(data);
lines.enter()
.append('path')
.attr('class', 'line');
lines
.attr('stroke', color)
.attr('d', line);
I don't see the line anymore, not at first, not after updates.
I'm including a codepen with the code in this question: https://codepen.io/anon/pen/BJaVNM?editors=0010
Thanks again for your help!

The proper data join would be:
// give data an array, each part of the array is an array representing a line (ie path)
let lines = g.selectAll('.line').data([data]); //<-- notice array
// you have lines entering, .merge this back to the update selection
lines = lines.enter()
.append('path')
.attr('class', 'line')
.merge(lines);
// lines variable is now enter + update
lines
.attr('stroke', color)
.attr('d', line);
Updated codepen

Related

Using d3 selection.join to create a new line each time

In the d3 examples of selection.join it seems there is an assumption that you already have a path to manipulate:
svg.selectAll("path")
.data([dataset])
.join(
enter => enter.append("path").attr("class", "line"),
update => update,
exit => exit.remove()
)
.attr("d", line);
However, I'd like to create a re-usable class component which creates a new line each time for that specific data. It should not interfere with other paths that may or may not be present in the entire graph.
const lineGenerator = d3
.line<DataPoint>()
.y((d) => yScale(d.value))
.x((d) => xScale(d.date));
mainSvg.selectAll('path')
.data([data])
.join(
enter =>
enter
.append('path')
.attr('class', 'line')
.attr('stroke', 'black'),
).attr('d', lineGenerator)
What I now see happening in a graph that already has a path somewhere is that that occurance is replaced with this new line. How would you typically solve this in d3?
Found a possible solution. Not sure if this is the way to go:
const lineGenerator = d3
.line<DataPoint>()
.y((d) => yScale(d.value))
.x((d) => xScale(d.date));
mainSvg.selectAll('#my-unique-id')
.data([data])
.join(
enter =>
enter
.append('path')
.attr('id', 'my-unique-id')
.attr('stroke', 'black'),
).attr('d', lineGenerator)
It looks like you can select something that is non-existent. If d3 can't find that occurrence it will create that instance.
Using a unique id for the new path you can make sure only the occurance is redrawn that you want to target.
I'm not sure if there are any caveats to this method, so any tips are appreciated.

d3.selectAll get the style fill value of map's path

I am trying to get the colour from the path but it gives me initially assigned colour. Initially, I filled the whole path with yellow colour(see below code):
# Line 566:
india.selectAll("path")
.data(fixed)
.enter().append("path")
.attr("d", path)
.style("fill", "#ffffd9")
but using the following code I assigned them diff. colour based on value:
# Line 620:
india.selectAll("path").transition().duration(1000)
.style("fill", function (d) {
// log("=======>",d, d.confirmed);
return colormap(d.properties.confirmed); });
This is what I have tried:
# Line 642:
rects = india.selectAll('path');
// clear previous selection
// rects.style({'stroke': 'none', 'stroke-width': '1px'});
// loop and hightlight matches
rects.each(function(){
var r = d3.select(this);
log("-----======--------", r._groups[0][0]);
log("!!!!!!!!!:",r._groups[0][0].style.cssText);
log("-->==>",r._groups[0][0].path);
log("#####:", r.style('fill'), hexToRGB('#fdedec'));
log("######:", (r.style('fill')===hexToRGB('#fdedec')));
// if (r.style('fill') === self.style('fill')){
// r.style({'stroke': 'red', 'stroke-width': '2px'});
});
But it gives me initially assigned colour(yellow), not the changed ones(shades of red). How to do it?
Code + demo:
https://plnkr.co/edit/4tsSOBuNhi418Pt5?open=lib%2Fscript.js

D3 js multiple line graph toggle dots on/off

This D3 js example shows all the code to produce a multi-line graph that can be toggled. Each line in the graph includes dots for existing data points.
While the lines can be toggled on/off, the dots stay stagnant. I would like for the toggle to work for both turning on/off the line & the dots that are associated with the same line.
I suspect that the svg.append("text") is the part that requires code update to also enable the dots to be turned on/off along with the line.
Here is the existing code snipet that turns on/off the line graph, but it doesn't turn on/off the dots.
svg.append("text")
.attr("x", (legendSpace/2)+i*legendSpace) // space legend
.attr("y", height + (margin.bottom/2)+ 5)
.attr("class", "legend") // style the legend
.style("font-size","15px") // Change the font size
.style("font-weight", "bold") // Change the font to bold
.style("text-anchor", "middle") // center the legend
.style("fill", function() { // Add the colours dynamically
return d.color = color(d.key); })
.on("click", function(){
// Determine if current line is visible
var active = d.active ? false : true,
newOpacity = active ? 0 : 1;
// Hide or show the elements based on the ID
d3.select("#tag"+d.key.replace(/\s+/g, ''))
.transition().duration(100)
.style("opacity", newOpacity);
// Update whether or not the elements are active
d.active = active;
})
.text(d.key);
Please help.
IDs are unique. You cannot set the same ID for several different DOM elements.
Solution: set classes instead.
For the lines:
.attr("class", 'tag'+d.key.replace(/\s+/g, ''))
And for the circles:
.attr("class", d=>'tag'+d.symbol.replace(/\s+/g, ''))
Then, get the class on the click event (use selectAll, not select):
d3.selectAll(".tag"+d.key.replace(/\s+/g, ''))
here is the updated fiddle: http://jsfiddle.net/gx4zc8tq/

Multiple kernel density estimations in one d3.js chart

I'm trying to make a plot that shows density estimations for two different distributions simultaneously, like this:
The data is in two columns of a CSV file. I've modified code from Mike Bostock's block on kernel density estimation, and have managed to make a plot that does what I want, but only if I manually specify the two separate density plots -- see this JSFiddle, particularly beginning at line 66:
svg.append("path")
.datum(kde(cola))
.attr("class", "area")
.attr("d", area)
.style("fill", "#a6cee3");
svg.append("path")
.datum(kde(colb))
.attr("class", "area")
.attr("d", area)
.style("fill", "#b2df8a");
I've tried various incantations with map() to try to get the data into a single object that can be used to set the color of each density area according to the color domain, e.g.:
var columns = color.domain().map(function(column) {
return {
column: column,
values: data.map(function(d) {
return {kde: kde(d[column])};
})
};
});
I don't have a great grasp of what map() does, but this definitely does not work.
How can I structure my data to make this plot in a less brittle way?
To make this generic and remove column dependency first prepare your data:
var newData = {};
// Columns should be numeric
data.forEach(function(d) {
//iterate over all the keys
d3.keys(d).forEach(function(col){
if (!newData[col])
newData[col] = [];//create an array if not present.
newData[col].push(+d[col])
});
});
Now newData will hold the data like this
{
a:[123, 23, 45 ...],
b: [34,567, 45, ...]
}
Next make the color domain like this:
var color = d3.scale.category10()
.domain(d3.keys(newData))//this will return the columns
.range(["#a6cee3", "#b2df8a"]);
Finally make your area chart like this:
d3.keys(newData).forEach(function(d){
svg.append("path")
.datum(kde(newData[d]))
.attr("class", "area")
.attr("d", area)
.style("fill", color(d));
})
So now the code will have no dependency over the column names and its generic.
working code here

D3 trouble while appending text on mouseover

I would like to show a text on mouseover.
var circle = elemEnter.append("circle")
.attr("r", function(d){return d.r*2} )
.attr("dx", function(d){return d.x} )
.attr("stroke","gray")
.attr("fill", "#91c6ed")
.on("mouseover", function()
{d3.select(this).style("stroke", "#ff7f0e");
d3.select(this).style("stroke-width", "2px");
elemEnter.append("text")
.text(function(d){return d.name})})
.on("mouseout", function()
{d3.select(this).style("stroke", "gray");
d3.select(this).style("stroke-width", "1px");});
This piece of code works but show all the names on all the circles and when I try to replace
elemEnter.append("text").text(function(d){return d.name})
by
d3.select(this).append("text").text(d.name)
nothing happens.
I think it is possible to do it but I don't know what I'm doing wrong.
You can't append text to a circle. You need to start with a g and append the circle to the g and append the text to the g. Keep in mind that the g will not have cx/cy attributes and so you'll need to put that data into the following syntax:
.attr("transform", function (d) {return "translate("+d.X+","+d.Y")"})
If you're binding data, bind it to the g and then just naked append the circle and text to the g.

Resources