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

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]);
});
}

Related

Rendering in the background of a dc.js chart with renderlet

I use dc.js for showing the results of multiple classification algorithms. More specifically, I want to show a precision recall chart (each point corresponds to a result of a classification system).
I already used a dc.js scatter chart for this which works fine.
Additionally I would like to have a d3 contour in the background of the chart which shows the F-measure.
This is already implemented. The only issue is that the contour part is in the foreground and not in the background of the chart.
Please have a look at the jsfiddle for a full example.
Two questions are still open for me because I'm not a dc.js or d3 expert:
Is there a way to put the contour in the background or the symbols(cycles) of the scatter chart in the foreground (I already tried it with the help of this stackoverflow question but with no success)
I used the 'g.brush' selector to get the area of the inner chart. This works fine as long as the brushing is turned on. Is the selector a good way to go or are there better alternatives (which may also work if brushing is switched off).
In my example I put the contour part in the upper left corner to see if it works but I also provide the code (currently uncommented) to increase the width and height of the contour to the correct size.
chart
.on('renderlet', function (chart) {
var innerChart = chart.select('g.brush');
var width = 300, height=300;
//getting the correct width, height
//var innerChartBoundingRect = innerChart.node().getBoundingClientRect();
//var width = innerChartBoundingRect.width, height=innerChartBoundingRect.height;
[contours, color] = generateFmeasureContours(width,height, 1);
innerChart
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
var symbols = chart.chartBodyG().selectAll('path.symbol');
symbols.moveToFront();
});
jsfiddle
Putting something in the background is a general purpose SVG skill.
SVG renders everything in the order it is declared, from back to front, so the key is to put your content syntactically before everything else in the chart.
I recommend encapsulating it in an svg <g> element, and to get the order right you can use d3-selection's insert method and the :first-child CSS selector instead of append:
.on('pretransition', function (chart) {
// add contour layer to back (beginning of svg) only if it doesn't exist
var contourLayer = chart.g().selectAll('g.contour-layer').data([0]);
contourLayer = contourLayer
.enter().insert('g', ':first-child')
.attr('class', 'contour-layer')
.attr('transform', 'translate(' + [chart.margins().left,chart.margins().top].join(',') + ')')
.merge(contourLayer);
A few more points on this implementation:
use dc's pretransition event because it happens immediately after rendering and redrawing (whereas renderlet waits for transitions to complete)
the pattern .data([0]).enter() adds the element only if it doesn't exist. (It binds a 1-element array; it doesn't matter what that element is.) This matters because the event handler will get called on every redraw and we don't want to keep adding layers.
we give our layer the distinct class name contour-layer so that we can identify it, and so the add-once pattern works
contourLayer = contourLayer.enter().insert(...)...merge(contourLayer) is another common D3 pattern to insert stuff and merge it back into the selection so that we treat insertion and modification the same later on. This would probably be simpler with the newer selection.join method but tbh I haven't tried that yet.
(I think there may also have been some improvements in ordering that might be easier than insert, but again, I'm going with what I know works.)
finally, we fetch the upper-left offset from the margin mixin
Next, we can retrieve the width and height of the actual chart body using
(sigh, undocumented) methods from dc.marginMixin:
var width = chart.effectiveWidth(), height = chart.effectiveHeight();
And we don't need to move dots to front or any of that; the rest of your code is as before except we use this new layer instead of drawing to the brushing layer:
contourLayer
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
Fork of your fiddle.
Again, if you'd like to collaborate on getting a contour example into dc.js, that would be awesome!

dragging line in linechart: update line to new values?

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!

Stroke rendering inside of Topojson area. How to change the style of the full shape?

I am having an issue with changing the stroke of an individual element in a Topojson file where my mouseover is not altering the stroke of the full shape.
A visual is probably best:
I would like every border element from the county shape area to receive the same stroke on mouse-over. Instead, I am getting this odd effect where only part of the border changes stroke.
I ended up using a solution like this, referenced here.
.on("mouseover", function(d,i) {
d3.select(this.parentNode.appendChild(this)).transition().duration(300)
.style({'stroke-opacity':1,'stroke':'#F00'});
})
Say all your drawn shapes are in a data-bound d3 selection called shapes (which you create using the usual enter, update, exit flow). Then something like this should work:
shapes.on('mouseover', function(d, i) {
// d is the datum of the hovered shape
// data is all the data currently bound to shapes
var data = shapes.data();
// this'll sort the data such that the hovered d is last in the array
data.sort(function(a,b) { return d3.ascending(a == d, b == d); })
// now that the data is sorted, reorder the shapes to match
// the order within data.
shapes.data(data);// NOTE: there's a good chance this line is not necessary. Try taking it out.
shapes.order();
});

in d3.geo MultiPoint how can I provide different shapes for different poins?

I have some geoJson data that I am charting using d3.geo.
When I write something like
d3.select("svg")
...
.attr("d", function(d) {
return path({
type:"MultiPoint",
coordinates: get_activity_coords_(d.activities)
});
})
I always get a circle for each coordinate. The coordinates represent locations of various stopping points of a journey. What I would prefer is a different shape for the first and the last coordinate.
Is it possible to do this using MultiPoint, is there an example that I can follow? I could draw the points one by one, but I recall reading that MultiPoint is far faster. Plus, the code would be much clearer to read.
Thanks a lot.
You can't do different shapes for MultiPoint geoJSON with d3.geo.path. You can change the radius based on a function, but it looks like you can only set it per feature and not per point, so you'd have to break your set of points into multiple features and lose any performance benefit from using the single element.
However, there are other ways to go about doing this.
One option, as you mentioned, is to create a nested selection with a separate <path> element for each point, and draw each path using a d3.svg.symbol() function. You can then customize the symbol function to be based on data or index.
var trips = d3.select("svg").selectAll("g.trips")
.data(/*The data you were currently using for each path,
now gets to a group of paths */)
.attr("class", "trips");
//also set any other properties for the each trip as a whole
var pointSymbol = d3.svg.symbol().type(function(d,i){
if (i === 0)
//this is the first point within its groups
return "cross";
if ( this === this.parentNode.querySelector("path:last-of-type") )
//this is the last point within its group
return "square";
//else:
return "circle";
});
var points = trips.selectAll("path")
.data(function(d) {
return get_activity_coords_(d.activities);
//return the array of point objects
})
.attr("transform", function(d){
/* calculate the position of the point using
your projection function directly */
})
.attr("d", pointSymbol);
Another option, which allows you to set custom shapes for the first and last point (but all intermediary points would be the same) is to connect the points as the vertices of a single, invisible <path> element and use line markers to draw the point symbols.
Your approach would be:
Create a <defs> element within your SVG (either hard-coded or dynamically with d3), and define the start, middle and end marker points within them. (You can use d3.svg.symbol() functions to draw the paths, or make your own, or use images, it's up to you.)
Use a d3.svg.line() function to create the path's "d" attribute based on your array of point coordinates; the x and y accessor functions for the line should use the projection function that you're using for the map to get the x/y position from the coordinates of that point. To avoid calculating the projection twice, you can save the projected coordinates in the data object:
var multipointLine = d3.svg.line()
.x(function(d,i) {
d.projectedCoords = projection(d);
return d.projectedCoords[0];
})
.y(function(d){ return d.projectedCoords[1];});
(You can't use your d3.geo.path() function to draw the lines as a map feature, because it will break the line into curves to match the curves of longitude and latitude lines in your map projection; to get the line markers to work, the path needs to be just a simple straight-line connection between points.)
Set the style on that path to be no stroke and no fill, so the line itself doesn't show up, but then set the marker-start, marker-mid and marker-end properties on the line to reference the id values of the correct marker element.
To get you started, here's an example using d3 to dynamically-generate line markers:
Is it possible to use d3.svg.symbol along with svg.marker

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.

Resources