Troubleshooting: D3 line transition is really jerky. Needs a remove()? - javascript

I've got a line graph with 159 lines. When the graph first loads, I want them all to graph as a straight line, y=5. That works. Then I want them to beautifully transition to their bumpy selves.
Well, my dang transition is jerky, not beautiful. I think I need a remove() somewhere in here.
Graph is here. Ignore the drop-downs; they're not part of the original load.
http://bl.ocks.org/greencracker/raw/f37dc463afa15bf17d91/
I think the key part may be in here:
var line = d3.svg.line()
.interpolate("basis")
.x(function (d) {return x(d.date); })
.y(function (d) {return y(d.rate) ; });
var flat_line = d3.svg.line()
.interpolate("basis")
.x(function (d, i) {return x(d.date); })
.y(function (d) {return y(5) ; });
Then further down:
function lineAnimate(selection) { // selection must be paths
selection.transition().duration(1150).ease("linear")
.attr("d", function (d) {return line(d.values);})
.style("opacity", 1)
.style("stroke", "red")
}
var counties = svg.selectAll(".county")
.data(data, function (d) {return d.key;})
.enter().append("g")
.attr("class", function (d) {return d.key + " " + d.wia + " county";})
.text(function (d) {return d.key});
counties.append("path")
.attr("class", function (d) {return d.key + " " + d.wia + " county" + " line";})
.attr("id", function (d) {return "a" + d.wia;})
.attr("d", function (d) {return flat_line(d.values); }) // flat line y=5
.attr("stroke", "gray")
.attr("stroke-width", "1px")
.style("opacity", 0.0)
counties.selectAll("path.line")
.call(lineAnimate)

It's just a lot of work to animate 159 lines simultaneously, and there's probably not much to do about that.
However, the SVG renderer is further bogged down by having to determine the resulting fills of all these overlapping semi-transparent lines. I suspect you can get a significant boost if you do away with the opacity change.
Finally, you might be able to improve the "line morph" animation by staggering the animations; i.e. the first line would begin (and finish) its animation a bit before the 2nd line, then the 3rd, etc. This way, only a subset of the lines is animating at any given point.
You'd do this by adding something like .delay( function(d, i) { return i * 100; }) after the .transition() call. Might not be the effect you're going for though. Just a thought...

Related

How to coordinate interactions between multiple data visualizations, particularly when one of them uses nesting? JavaScript and D3

For a project I am attempting to have three visualizations for data based on car stats, where if you hover over one, the others will show the affects of that hovering as well.
The first is a bar graph, the second is a scatterplot, and the third is a line graph. For the line graph I wanted to group by manufacturer so that I don't have a couple hundred lines on my line graph, as the plot coordinates on the x and y are acceleration and model year. The other two don't need to be grouped in this way because one of their axes is the manufacturer.
I have the interactions from the line graph to the other two working since there is no nesting on the bar or scatterplot, and both the scatterplot and the bar graph can affect each other perfectly fine, but since the data is nested for the line graph, I can't seem to figure out how to access it, as the way I was doing it for the other two (using filtering) does not seem to work.
Below I am first showing where I am trying to create interactions when the mouse hovers (this is for the bar graph), and below that I include how my line graph is set up to show how it works. All I want is to make the corresponding line stand out more from the others by thickening the stroke when I hover over the bar or plot (in the scatterplot), and then go back to the normal size upon moving my cursor.
I followed the tutorial on the D3 website for line graphs, so there shouldn't be anything particularly wrong with that code.
Creating the bars for the bar graph, the mouseover and mouseout are the important parts:
var path1 = svg1.selectAll("myRect")
.data(data)
.enter()
.append("rect")
.attr("x", x1(0.1) )
.attr("y", function(d) { return y1(d.Manufacturer); })
.attr("height", y1.bandwidth() )
.attr("width", function(d) { return x1(d.Cylinders); })
.attr("fill", function (d) {
return color1(d.Cylinders);
})
.on('mouseover', function (d, i) {
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr("fill", function (d) {
return color4(d.Cylinders);
})
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr('r', 9)
.attr("fill", function (d) {
return color5(d.Horsepower);
});
svg3.selectAll('path') //THIS IS THE LINE GRAPH
.filter(function(f) {
console.log(this)
return ; // <-------This is where I don't know what to return to just get one line
})
.attr("stroke-width", 7)
})
.on('mouseout', function (d, i) {
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr("fill", function (d) {
return color1(d.Cylinders);
});
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr('r', 5)
.attr("fill", function (d) {
return color2(d.Acceleration);
});
d3.selectAll('path') //DELESLECTING LINE GRAPH
.filter(function(f) {
return f.key === d.Manufacturer; //this is what I tried before but it doesn't work
})
.attr("stroke-width", 1.5)
});
Creating the line graph:
var sumstat = d3.nest()
.key(function(d) { return d.Manufacturer;})
.entries(data);
// Add X axis
var x3 = d3.scaleLinear()
.domain([69, 84])
.range([ 0, width3 ]);
svg3.append("g")
.attr("transform", "translate(0," + height3 + ")")
.call(d3.axisBottom(x3).ticks(5));
// Add Y axis
var y3 = d3.scaleLinear()
.domain([8, d3.max(data, function(d) { return +d.Acceleration; })])
.range([ height3, 0 ]);
svg3.append("g")
.call(d3.axisLeft(y3));
var div3 = d3.select("#my_div").append("div")
.attr("class", "#tool_tip")
.style("opacity", 0)
.style("font-size", "xx-large");
// color palette
var res = sumstat.map(function(d){ return d.key }) // list of group names
var color = d3.scaleOrdinal()
.domain(res)
.range(['darkolivegreen','darkred','palevioletred','indianred', 'hotpink'])
// Draw the line
svg3.selectAll(".line")
.data(sumstat)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 1.5)
.attr("d", function(d){
return d3.line()
.x(function(d) { return x3(d.ModelYear); })
.y(function(d) { return y3(+d.Acceleration); })
(d.values)
})
.on('mouseover', function (d, i) {
//highlight;
svg3.selectAll("path")
.attr("stroke-width", 0.9)
d3.select(this)
.attr("stroke", function(d){ return color(d.key)})
.attr("stroke-width", 6)
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr("fill", function (d) {
return color4(d.Cylinders);
})
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr('r', 9)
.attr("fill", function (d) {
return color5(d.Horsepower);
});
})
.on('mouseout', function (d, i) {
svg3.selectAll("path")
.attr("stroke-width", 1.5)
d3.select(this)
.attr("stroke", function(d){ return color(d.key)})
.attr("stroke-width", 1.5)
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr("fill", function (d) {
return color1(d.Cylinders);
})
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr('r', 5)
.attr("fill", function (d) {
return color2(d.Horsepower);
});
});
Any assistance I can get would be greatly appreciated!!
I think I may have figured out the problem. It would seem that trying to filter the paths causes an issue because the x and y axes are also technically lines, and thus have paths that are null. I tried
svg3.selectAll('path')
.filter(function(f) {
console.log(f)
if(f!=null)
return f.key === d.Manufacturer;
})
.attr("stroke-width",7)
In the .on('mouseover') function, and it seems to be working. The issue was the nulls, not the actual accessing of the keys.
Still taking suggestions if there is a better way to do this!

Using General update pattern in line graph

I have a demo here
Its a line bar chart using D3 in an Angular app.
I want the chart to be responsive so when the page is resized the chart width will increase and the height will be stay the same.
I'm doing this by capturing the window resize and then calling the function that draws the chart.
This works for the axis but I cant get the line and points to redraw.
I think it's to do with the way I'm trying to us the update pattern
How can I use the update pattern to redraw this line graph
const that = this;
const valueline = d3.line()
.x(function (d, i) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.y(function (d) {
return that.y(d.value);
});
this.x.domain(data.map((d: any) => d.date));
this.y.domain(d3.extent(data, function (d) {
return d.value
}));
const thisLine = this.chart.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
const totalLength = thisLine.node().getTotalLength();
thisLine.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength);
thisLine.transition()
.duration(1500)
.attr("stroke-dashoffset", 0)
let circle = this.chart.selectAll("line-circle")
.data(data);
circle = circle
.enter()
.append("circle")
.attr("class", "line-circle")
.attr("r", 4)
.attr("cx", function (d) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.attr("cy", function (d) {
return that.y(d.value);
})
circle
.attr("r", 4)
.attr("cx", function (d) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.attr("cy", function (d) {
return that.y(d.value);
})
circle
.exit()
.remove()
You have problems in both circles' selection and the line selection.
The circles' selection:
You're selecting "line-circle". Instead of that, you have to select by class: ".line-circle";
You're reassigning the circle selection:
circle = circle.enter()//etc...
Don't do that, otherwise circle will point to the enter selection, not to the update selection anymore. Just do:
circle.enter()//etc...
The path:
You're appending a new path every time you call the function. Don't do that. Instead, select the existing path and change its d attribute, or append a new path if there is none. Both behaviours can be achieved with this code:
let thisLine = this.chart.selectAll(".line")
.data([data]);
thisLine = thisLine.enter()
.append("path")
.attr("class", "line")
.merge(thisLine)
.attr("d", valueline);
Here is your forked code: https://stackblitz.com/edit/basic-scatter-mt-vvdxqr?file=src/app/bar-chart.ts

Transitioning basis line to linear line shortens/hide part of the line

I have done some work with d3.js but I'm stuck on this problem. I have a multi-line graph which starts with smooth lines and circles.
What I want to happen is when I toggle a legend button, the line(s) are supposed to transition between basis (smooth) and linear (normal). However, when I click the button I notice that the transition from basis to linear happens but then part of the line erases and doesn't match up with the points.
In order to create the lines I have two line functions. One for linear and one for basis.
var lineGen = d3.svg.line()
.interpolate("linear")
.x(function(d) {
return xScale(+d.tx_week);
})
.y(function(d) {
return yScale(d.RelativeChange);
});
var lineMean = d3.svg.line()
.interpolate("basis")
.x(function(d) {
return xScale(+d.week);
})
.y(function(d) {
return yScale(d.amount);
});
Then I create the basis line calling the lineMean function for the d attribute
var line = tx_year_grp.selectAll("path")
.data(function(d) { return [d]; })
.enter().append("svg:path")
.attr("class", "line")
.attr("fill", "none")
.attr("d", function(d) { return lineMean(d.values); })
.style('stroke', function(d) {return color(d.key); })
.on('mouseover', function(){
var sel = d3.select(this)
.style('stroke-width', 4)
this.parentNode.parentNode.appendChild(this.parentNode);
})
.on('mouseout', function(d) {
d3.select(this)
.transition()
.duration(750)
.style('stroke-width', 2)
});
Then I add transitions to all lines created.
for(u=0; u < nested_data.length; u++){
var pathId = document.getElementById('tx_year_grp' + nested_data[u].key);
var updPath = d3.select(pathId).selectAll("path");
var totalLength = updPath.node().getTotalLength();
updPath
.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(0)
.ease("linear")
.attr("stroke-dashoffset", 0)
}//end for
This does exactly what I want and creates a basis line. When I toggle my legend button to make the line linear and connect to the points, the transition occurs but then the line erases or overwrites itself so the line appears shorter and doesn't connect with all of the points.
Here is my code for changing the line. I call the lineGen function which switches the interpolation to 'linear'.
var linearLine = tx_year_grp.selectAll(".line");
linearLine.transition()
.duration(3000)
.attr("d", function(d) { return lineGen(d.values); });
This seems fairly straightforward but I'm doing something wrong.
Ok here's my interpretation:
The linear path will be longer than the basis path.
But it will still have the stroke dash-array you set to be the length of the basis path and then be blank for another equivalent length - .attr("stroke-dasharray", totalLength + " " + totalLength) (why do you have a stroke dash-array?). Therefore any portion of your linear line that's longer in path length than the basis line will be blank as it will extend into the empty bit of the stroke dash pattern. Try removing the stroke-dash property to see.
If this doesn't work, try making a jsfiddle so people can have a poke about

d3 Multiline Graph Update

I have a multiline graph that displays 10 series of data, I am trying to get the lines to update with new data but for some reason I can't get that happening.
The transition with the new data is working for the points on the lines so I assume I am not selecting the right elements but for the life of me I can't figure out where my mistake is.
At one point I had one line changing which indicated it was only updating from the first index of the data array.
Any insight would be appreciated:
Initial Series creation-
var series = svg.selectAll(".series")
.data(seriesData)
.enter().append("g")
.attr("class", "series");
series.append("path")
.attr("id", function (d) {
return d.name;
})
.attr("stay", "false")
.attr("class", "line")
.attr("d", function (d) {
d.line = this;
return line(d.values);
})
.attr("opacity", ".2")
.on("click", function () {
fadeOuts(this);
})
.style("stroke", function (d) {
return strokeCol;
})
.style("stroke-width", "4px")
.style("fill", "none");
Update function:
This is where I am stuck, the points respond to the new data but the paths do not.
series.data(newseriesData);
series.selectAll("path")
.attr("id", function (d) {
return d.name;
})
.attr("d", function (d) {
d.line = this;
return line(d.values);
})
.attr("opacity", ".2")
.on("click", function () {
fadeOuts(this);
})
.style("stroke", function (d) {
return strokeCol;
})
.style("stroke-width", "4px")
.style("fill", "none");
series.selectAll(".point")
.data(function (d) {
return d.values;
})
.transition()
.attr("cx", function (d) {
return x(d.label) + x.rangeBand() / 2;
})
.attr("cy", function (d) {
return y(d.value);
})
.style("fill", function (d) {
return color(d.name);
})
.style("stroke", "grey")
.style("stroke-width", "2px")
.on("mouseover", function (d) {
showPopover.call(this, d);
})
.on("mouseout", function (d) {
removePopovers();
})
Yes this is a university project, this is the last piece of work in a solid 50+ hour effort on this and I'd just like to get it knocked out.
The short answer is that instead of series.selectAll("path") you should use series.select("path"). Remember that series is already a selection, and the subselection is done for each element in it. You've appended exactly one element to each of the selection, so .select() is fine and no .selectAll() is required.
The main difference this makes is that .select() inherits the data from the parent selection, while .selectAll() doesn't -- when doing .selectAll() the data is simply not updated and therefore no change occurs.

d3 Animate Multiple SVG Lines

I'm trying to create multiple lines on a line graph one at a time. I've created an object array of about 100 lines in the below format:
var allLines = [{type: "linear", values: [1000, 2000, 3000]}, {}, ... {}];
var line = d3.svg.line()
.defined(function (d) {
return d != null;
})
.x(function (d, i) {
return x(new Date(minYear + i, 1, 1));
})
.y(function (d) {
return y(d);
});
Now I want to draw each line, one at a time with a delay of about 250 milliseconds between each line. I've tried the below approach which I thought would work, but I must be missing something because it just waits 250ms and then draws all the lines.
svg.append('g')
.attr('class', 'lineGroup')
.selectAll('path')
.data(allLines)
.enter()
.append('path')
.attr('class', function (d) {
return d.type;
})
.style('visibility', 'hidden')
.attr('d', function (d) {
return line(d.values);
});
function foo(transition) {
transition
.style('visibility', 'visible');
}
d3.select('.lineGroup').selectAll('path').transition().delay(250).call(foo);
Your basic approach is right, you just need to adjust the delay dynamically such that the later lines are drawn later. At the moment, the delay applies to all lines. To make it dynamic, you can use something like
svg.append("g")
// etc
.transition()
.delay(function(d, i) { return i * 250; })
.style('visibility', 'visible');
You can also do everything in one call, no need for a separate one after creating the lines.

Categories

Resources