dragging line in linechart: update line to new values? - d3.js

Yet another d3.js dragging/updating question: I previously got help on creating linecharts where you could drag a dot and the connecting line would adjust. Now I am trying to modify this further so that it is possible to both drag a dot (to move just one datapoint) OR drag the line to move all dots/the entire series represented by the selected line.
I think I have most of it sorted now - expect for the correct updating of the dragged line.
Following a linedrag, the data is updated correctly but I can't get the line to redraw as I want. I think part if it may be due to the use of a d3.line function to draw the connectlines - I think there is some kind of spilling over happening between the paths of the connectline and the paths of the axes.
This is the code to update the lines that works partly yet in mysterious ways:
focus.selectAll("path")
.merge(focus.selectAll('connectline'))
.data(dByTime)
.attr("d", function(d) {
return connectLine(d.values);
})
.attr("stroke", function(d) {
return color(d.key);
})
.attr('stroke-width', 4)
.style("fill", "none")
.style('cursor', 'pointer');
So: this works but doesn't. When selecting the dark line (tt:2) at first, there is a moving line of the correct shape etc but it's located too low (by about the y-axis range I think?) and the old connectline does not disappear while the x-axis path does disappear. The new (lower) line has full functionality in that if you move one of the dots associated with the line, the line gets adjusted, and you can also move the line again and see the dots AND line move....!?
When dragging the light blue line first (tt:1), the same happens, but when you repeat drag it, the y axis path also disappears and eventually a fully working connect line appears for the dark blue (tt 2) series...
A fiddle of the full code is here: https://jsfiddle.net/m931k6w2/
I am a) perplexed by the difference in location for the newly appearing lines relative to the old lines, the y axis and the updated values in the data and b) unable to isolate the paths of the connectline from the other paths and wonder if I should not find a workaround that does not use:
var connectLine = d3.line()
.x(function(d) {
return x(d.tt);
})
.y(function(d) {
return y(d.RT);
});
yet I don't know how else to draw the lines in the first place.
Any helps much appreciated!

Related

D3 selection causing incomprehensibely overlapping line plots---how do I fix it?

I am trying to draw a path on a Leaflet map using a D3 overlay, via the Leaflet.D3SvgOverlay utility library.
I inspected the output of the routing functions that I wrote using mplleaflet in Python, and got very neat output (responsible block).
I wrote a D3 thing that ought to get the same output. However I instead got a very jagged line, which, on closer visual inspection, turned out to be several lines intertwined in a weird way:
Changing the plot to one of circles instead of of a path shows that this is due to some sort of strangely arrayed points:
You can try out the responsible code yourself in this block.
Yet in both cases (the mplleaflet plot that works and the D3 plot that doesn't) I am merely trying to plot the same exact series of coordinates:
[[-73.98208, 40.76529], [-73.98225, 40.76476], [-73.98232, 40.76457], [-73.98238, 40.76441], [-73.98239, 40.76438], [-73.98241, 40.76434], [-73.98245, 40.76423], [-73.98249, 40.76412], [-73.98252, 40.76405], [-73.98254, 40.76402], [-73.98257, 40.76396], [-73.98281, 40.76351], [-73.98288, 40.76339], [-73.98293, 40.7633], [-73.983, 40.76318], [-73.98321, 40.76282], [-73.98326, 40.76273], [-73.98332, 40.76264], [-73.9836, 40.76219], [-73.98368, 40.76207], [-73.98387, 40.76178], [-73.98411, 40.76145], [-73.98452, 40.7608], [-73.98456, 40.76072], [-73.98465, 40.76052], [-73.98487, 40.76013], [-73.98487, 40.76013], [-73.98453, 40.75999], [-73.98418, 40.75984], [-73.98418, 40.75984], [-73.98441, 40.75952], [-73.98461, 40.7592], [-73.98507, 40.75858], [-73.98526, 40.75833], [-73.98553, 40.75796], [-73.98568, 40.75778], [-73.986, 40.75734], [-73.98648, 40.7567], [-73.98695, 40.75603], [-73.98695, 40.75603], [-73.98644, 40.75582], [-73.98644, 40.75582], [-73.98668, 40.75505], [-73.98691, 40.75434], [-73.98713, 40.75362], [-73.98723, 40.75332], [-73.98732, 40.75303], [-73.98735, 40.75291], [-73.98754, 40.75218], [-73.98761, 40.75187], [-73.98762, 40.75183], [-73.98771, 40.75145], [-73.98774, 40.7512], [-73.98783, 40.7507], [-73.98789, 40.75036], [-73.98798, 40.74988], [-73.98804, 40.74968], [-73.98809, 40.74957], [-73.98814, 40.74948], [-73.9883, 40.74925], [-73.9883, 40.74924], [-73.98829, 40.74923], [-73.98819, 40.74919], [-73.98804, 40.74912], [-73.98802, 40.74912], [-73.98802, 40.74911], [-73.98801, 40.7491], [-73.98803, 40.749], [-73.98805, 40.74898], [-73.98807, 40.74896], [-73.98808, 40.74894], [-73.9881, 40.74881], [-73.98812, 40.74869], [-73.98813, 40.74863], [-73.98814, 40.74858], [-73.98817, 40.74845], [-73.9882, 40.74831], [-73.98824, 40.74811], [-73.98836, 40.74754], [-73.98839, 40.74735], [-73.98841, 40.74721], [-73.98843, 40.74713], [-73.98844, 40.74707], [-73.98849, 40.74679], [-73.98858, 40.74604], [-73.98867, 40.74582], [-73.98872, 40.74559], [-73.98877, 40.7453], [-73.9889, 40.74455], [-73.98894, 40.74435], [-73.98899, 40.74412], [-73.98903, 40.74392], [-73.98904, 40.7438], [-73.98905, 40.74368], [-73.98911, 40.74337], [-73.98915, 40.74316], [-73.98917, 40.74303], [-73.98919, 40.74294], [-73.98924, 40.74261], [-73.98924, 40.74257], [-73.98924, 40.74253], [-73.98923, 40.74252], [-73.98922, 40.7425], [-73.98902, 40.74231], [-73.98902, 40.74231], [-73.98906, 40.74225], [-73.98931, 40.7419], [-73.9894, 40.7418], [-73.98959, 40.74155], [-73.9901, 40.74087], [-73.99053, 40.74025], [-73.99096, 40.73967], [-73.99139, 40.73908], [-73.99181, 40.73849], [-73.99222, 40.73792], [-73.99267, 40.73732], [-73.99315, 40.73669], [-73.99347, 40.73624], [-73.99352, 40.73618], [-73.99363, 40.73602], [-73.99416, 40.7353], [-73.99459, 40.73467], [-73.99507, 40.73403], [-73.99552, 40.73344], [-73.99594, 40.73284], [-73.99638, 40.73225], [-73.99668, 40.73185], [-73.99698, 40.73141], [-73.99698, 40.73141], [-73.9971, 40.73147], [-73.9985, 40.73216], [-73.99863, 40.73222], [-73.99863, 40.73222], [-73.99873, 40.73211], [-73.99913, 40.73164], [-73.99962, 40.73105], [-73.99962, 40.73105], [-73.99886, 40.73067], [-73.99886, 40.73067], [-73.99903, 40.73046]]
Inspecting the console shows that the D3 plot is plopping lines on top of one another over and over again, pointing to some sort of error on my part:
My question is: where in this code did I screw things up, and how do I fix it?
You have two main issues here.
Firstly, you are appending a new line to the overlay element each time the user zooms. This is the cause of the multiple lines on top of each other. The standard d3 approach is to only update the canvas when the underlying data changes, hence the better approach would be to only update the path when you enter new data. Note the use of the selectAll and enter() methods:
// Paints a single sampler path.
function paintPath(linearray) {
// Define x and y conversions.
var line = d3.svg.line()
.x(function(d) { return proj.latLngToLayerPoint(d).x})
.y(function(d) { return proj.latLngToLayerPoint(d).y});
var updateSelection = sel.selectAll('path').data([linearray]);
updateSelection.enter()
.append("path")
.attr({
"class": "sample-line",
"d": line,
"fill": "transparent",
"stroke": "steelblue",
"stroke-width": 0.1,
"shape-rendering": "crispEdges"
})
}
The second issue is with the latLngToPoint function. From the documentation, this function:
Projects geographical coordinates on a given zoom into pixel
coordinates
When you are zoomed out, the mapping to pixels is not very precise (hence the jagged line that you see). Because you aren't removing this line when the user zooms in, the additional lines that get appended overlay this initial jagged line, which also gets zoomed in. At a closer zoom level, the mapping to pixels more closely reflects the true coordinates, and you get a better line drawn. This explains the weird jagged overlay you are seeing.
This creates a bit of an issue, because merely calling .enter() is not going to update the line, as the underlying data has not changed. A simple solution (probably not perfect in terms of performance) would be to redraw the line each time the user zooms in:
// Paints a single sampler path.
function paintPath(linearray) {
// Define x and y conversions.
var line = d3.svg.line()
.x(function(d) { return proj.latLngToLayerPoint(d).x})
.y(function(d) { return proj.latLngToLayerPoint(d).y});
sel.selectAll('path').remove();
var updateSelection = sel.selectAll('path').data([linearray]);
updateSelection.enter()
.append("path")
.attr({
"class": "sample-line",
"d": line,
"fill": "transparent",
"stroke": "steelblue",
"stroke-width": 0.1,
"shape-rendering": "crispEdges"
})
}
The better solution may be to use the data returned from latLngToLayerPoint as your D3 data element. This way you will be able to update the line when you zoom in to a higher resolution. I will leave you to implement this.
To maintain constant line width upon zoom, you can use `"vector-effect":"non-scaling-stroke". Note the change in stroke width. This is almost something I would pull out into CSS.
.attr({
"class": "sample-line",
"d": line,
"fill": "transparent",
"stroke": "steelblue",
"stroke-width": 1,
"shape-rendering": "crispEdges",
"vector-effect": "non-scaling-stroke"
})
Edit: Ignore my comments about performance. It seems that even without removing and reappending lines (ie. just keeping the first line drawn), there is still a bit of lag when you scroll around the map. This is probably an issue on the Leaflet side.
Edit2: Note that I've also changed paintPathSampler to remove the second call to paintPath:
// Paints all of the paths.
function paintPathSampler() {
d3.json("path_sampler.json", function (data) {
paintPath(data[0]);
// paintPath(data[1]);
// paintPointPath(data[0]);
});
}

Using D3, how can I transition a line one point at time?

I'm working on a visualization project in which one component is a line chart overlayed on a bar graph. I delayed the bar transitions at a time. I would like the line to transition similarly so each point on the line remains "attached" to the bar.
Here's the code for the line:
var line = d3.svg.line()
.x(function(d, i) {
return xScale(i) + 20;
})
.y(function(d) {
return h - yScale(parseFloat(d.performance));
});
svg1.append("svg:path").attr("d", line(dataset[0].months));
And here's where I transition it:
svg1.select("path")
.transition()
.ease("linear")
.duration(1000)
.attr("d", line(dataset[count].months));
I've seen other questions addressing d3 line transitions but none that seem to address my issue, so I hope I'm not a repeater. Thanks in advance!
Note: I did try putting the delay() function in after transition which didn't work. I'm assuming this is because the line is a single <path> instead of multiple <rect> elements...
So this fell off my radar for a while, but I had some time the other day and figured out one approach for doing the delayed transition...
Here is the pen I wrote. I generated some random data and created a simple line chart to showing stock prices to play around with. The trick here is instead of iterating through a selection of elements using transition, we iterate through the dataset updating it point by point and transitioning the line as we go:
dataset.forEach(function(item, index) {
let set = dataset.slice();
// Update the current point in the copy
set[index].price = newPrice();
stock_line.transition()
.delay(index * 500)
.attr('d', line_generator(set));
});
Admittedly this is a bit hacky and possibly overkill, you could just update the whole line at once. Also #Lars mentioned the possibility of using the stroke-dashoffset trick to accomplish this as well. I have played around with that method to animate drawing the line but I'm not sure how I'd use it to accomplish the same delayed animation shown in the pen. If there is a less hacky implementation please let me know and I'll update this answer.

D3 multi line chart - strange animation

I have created a multi line chart with a simple animation. In the beginning there are no data and after clicking a button new values are emulated and the line "move" left. The move is animated using a "shift".
The problem occurs when the lines "fill" the whole graph area (that means there are y values for all x values) and then the lines are animated in a different way. It looks like the y values are animated on a curve, not slided to the left.
The animation works good for both axes:
svg.selectAll("g .x.axis")
.transition()
.duration(500)
.ease("linear")
.call(xAxis);
svg.selectAll("g .y.axis")
.transition()
.duration(500)
.ease("linear")
.call(yAxis);
And not for lines (this code helped me a lot)
svg.selectAll("g .city path")
.data(processedData).transition().duration(500)
.ease("linear")
.attr("d", function(d, i) { return line(d.values); })
.attr("transform", null);
The Fiddle is accessible here.
Thanks for help.
The problem is that you're deleting data when there is too much. The way d3 matches data to existing data (when you call the .data() function) is by index. That is, the first element in the array that you pass to .data() matches the first bound data element, regardless of what the data actually looks like.
What happens in your case is that as soon as you start deleting data, the individual data points are updated instead of shifted. That's why you're seeing the "squiggle" -- it's updating each data point to its new value, which is the value the data point to the right had before.
With the code you currently have, this is hard to fix because you are not matching the data for individual lines explicitly. I would recommend that you have a look at nested selections which allow you to draw multiple lines and still explicitly match the data for individual ones. The key is to use the optional second argument to .data() to supply a function that tells it how to match the data (see the documentation). This way you can tell it that some data points disappeared and the other ones should be shifted.
you can get around this problem in 2 step.
in function update() : redraw you .data() with the new point at the end but without remove the first old point (with animation), like that each key is the same before and after transition.
at the end of function update() : you can remove the old value and redraw .data() without animation.

error computing area of a closed space in a force layout using d3js

I have a graph which I've laid out using force layout. Basically i want to color the area contained within the cycles in the graph. Here's my code.
I was trying to paint the cycles when the force layout stabilized.
force.on("tick",function(){
// ... I update the position of nodes and the links
if(force.alpha() < 0.006){
force.stop();
// I dont know if there's an easier way of doing this
var xnodes = [];
force.nodes().forEach(function(d){
xnodes.push([d.x, d.y]);
});
// I tried creating a path and filling with with green
vis.select("path.area")
.data([xnodes])
.enter().append("path")
.style("fill", "#00ff00")
.attr("class", "area")
.attr("svg:d", d3.svg.area());
}
});
When i run it in chrome, the debugger shows that it does create a path but the area of the path is 0px x 0px. I'm really confused with this. I even tried setting the array manually. I get the same error.
var ynodes = [[352.3554660996234,304.3361316660001],[449.88953454311286,313.14153937680953],
[392.0458559272036,389.6656922220398],[352.3554660996234,304.3361316660001]];
vis.select("path.area")
.data([ynodes])
.enter().append("path")
.style("fill", "#00ff00")
.attr("class", "area")
.attr("svg:d", d3.svg.area());
However when i put this code in a blank html file (after you include the necessary libraries) it works fine. It does draw a path and fills it with green. I'm really confused here. Any help would be much appreciated. Thanks.

Update multi-line graph D3.js

I'm attempting to update a d3 multi-line graph by pulling data at 5 second intervals from a mySQL database using PHP. At the moment the graph is displaying but not updating - I know that there is something not right with my updateData function but have tried everything can think of and nothing seems to work. Can anyone please help?
https://gist.github.com/Majella/ab32fe0151fd487da3f6
UPDATE:
As you can see the x-axis line is only showing sporadically and some of the lines aren't lined up with the y-axis.
Updated gist:
https://gist.github.com/Majella/ab32fe0151fd487da3f6
UPDATE 2: For some bizarre reason the lines are changing colour - or moving completely not exactly sure. So while on graph above the lines are from top - blue, orange then white - when graph updating the blue might move to bottom with orange on top and white in middle etc - but happening randomly?
In your original drawing of the graph, you correctly use:
var parameter = svg.selectAll(".parameter")
.data(data, function(d) { return d.key; })
.enter().append("g")
.attr("class", "parameter");
which joins the data (data) to the elements (g.parameter)
During your update function, you will need to join the data again in order to perform updates, deletes, and adds of elements. The 3 little circles tutorial is an excellent place to learn more about this.
Anyway, in your update function, you may want something like this (untested):
// re-acquire joined data
var containers = svg.selectAll("g.parameter")
.data( data );
// update existing elements for this data
containers
.select( "path.line" )
.attr( "d", function(d) { return line(d.values); })

Resources