I'm trying to give a polygon - drawn with d3 - smooth edges using the d3.svg.line().interpolate() option but I get strange looking results.
I receive the polygon data from the nokia HERE api as world coordinate data in the form [lat1, long1, alt1, lat2, long2, alt2 ...] So in the routingCallback function - which is called when the response is in - I first refine it so it looks like this [[lat1, long1], [lat2, long2] ...]. In d3.svg.line() I then use this array of coordinates to calculate the pixel positions. Im using Leaflet to draw the polygon on a map so I use the map.latLngToLayerPoint() function to do that. The actual drawing of the polygon happens in reset() which is called from the routingCallback immediately after the data is available and every time the map gets zoomed
var map = new L.Map("map", {"center": [52.515, 13.38], zoom: 12})
.addLayer(new L.TileLayer('http://{s}.tile.cloudmade.com/---account key---/120322/256/{z}/{x}/{y}.png'));
map.on("viewreset", reset);
var svg = d3.select(map.getPanes().overlayPane).append("svg"),
g = svg.append("g").attr("class", "leaflet-zoom-hide group-element"),
bounds = [[],[]],
polygon,
refinedData,
line = d3.svg.line()
.x(function(d) {
var location = L.latLng(d[0], d[1]),
point = map.latLngToLayerPoint(location);
return point.x;
})
.y(function(d) {
var location = L.latLng(d[0], d[1]),
point = map.latLngToLayerPoint(location);
return point.y;
})
.interpolate("cardinal"),
routingCallback = function(observedRouter, key, value) {
if(value == "finished") {
var rawData = observedRouter.calculateIsolineResponse.isolines[0].asArray(),
refinedData = [];
for(var i = 2; i < rawData.length; i += 3) {
var lon = rawData[i-1],
lat = rawData[i-2];
refinedData.push([lat, lon]);
}
if(polygon)
polygon.remove();
polygon = g
.data([refinedData])
.append("path")
.style("stroke", "#000")
.style("fill", "none")
.attr("class", "isoline");
reset();
}
if(value == "failed") {
console.log(observedRouter.getErrorCause());
}
};
getIsolineData = function(isoline) {
return data;
};
function reset() {
var xExtent = d3.extent(refinedData, function(d) {
var location = L.latLng(d[0], d[1]);
var point = map.latLngToLayerPoint(location);
return point.x;
});
var yExtent = d3.extent(refinedData, function(d) {
var location = L.latLng(d[0], d[1]);
var point = map.latLngToLayerPoint(location);
return point.y;
});
bounds[0][0] = xExtent[0];
bounds[0][1] = yExtent[0];
bounds[1][0] = xExtent[1];
bounds[1][1] = yExtent[1];
var topLeft = bounds[0],
bottomRight = bounds[1];
svg .attr("width", bottomRight[0] - topLeft[0])
.attr("height", bottomRight[1] - topLeft[1])
.style("left", topLeft[0] + "px")
.style("top", topLeft[1] + "px");
g .attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
polygon.attr("d", line);
}
I expect this to produce smooth edges but instead I get a small loop at every corner. The red overlay is the same polygon without interpolation. There are only points at the corners. No points added inbetween.
Does it have something to do with the order of the points (clockwise/counter clockwise)? I tried to rearrange the points but nothing seemed to happen.
The only way I can recreate the pattern you're getting is if I add every vertex to the path twice. That wouldn't be noticeable with a linear interpolation, but causes the loops when the program tries to connect points smoothly.
http://fiddle.jshell.net/weuLs/
Edit:
Taking a closer look at your code, it looks like the problem is in your calculateIsolineResponse function; I don't see that name in the Leaflet API so I assume it's custom code. You'll need to debug that to figure out why you're duplicating points.
If you can't change that code, the simple solution would be to run your points array through a filter which removes the duplicated points:
refinedData = refinedData.filter(function(d,i,a){
return ( (!i) || (d[0] != a[i-1][0]) || (d[1] != a[i-1][1]) );
});
That filter will return true if either it's the first point in the array, or if either the lat or lon value is different from the previous point. Duplicated points will return false and be filtered out of the array.
Related
I'm trying to implement a tooltip on mouseover for a multi line chart.
I've followed the code from this example and tried to change it so that I see the X values of the lines for a given hovered Y value, but I'm not able to get it to work.
My attempt can be found below.
In my actual implementation I'm writing in Typescript and the functions 'getTotalLength()' and 'getPointAtLength()' are saying they don't exist on property Element.
Also if you can add a text box at on the line that has the hovered Y value that'd help me a lot!
https://codesandbox.io/s/modest-minsky-hvsms?fontsize=14&hidenavigation=1&theme=dark
Thanks
So after careful review there were several errors which I have corrected.
Your paths for the data lines were not assigned the class so you need to assign the class of dataLine to them when you append them like so:
svg
.selectAll(".dataLine")
.data(nestedData)
.enter()
.append("path")
.attr("fill", "none")
.attr("class", "dataLine")
.attr("stroke", d => itemMap(d.key).color)
.attr("stroke-width", d => itemMap(d.key).lineWeight)
.attr("d", d =>
d3
.line()
.x(d => x(d.xvalue))
.y(d => y(d.yvalue))(d.values)
);
As pointed out in the comment above, stop using arrow functions if you intend to use this. Once you do that, your d3.mouse(this) starts working.
The example you followed had the paths from left to right, while yours is from top to bottom. This required several changes in terms of coordinates to get the alignment of the mouseover line and the circles with the text values near them to align properly. The correct code is as follows:
.on("mousemove", function() {
//#ts-ignore
var mouse = d3.mouse(this);
d3.select(".mouse-line").attr("d", () => {
var d = "M" + plotWidth + "," + mouse[1];
d += " " + 0 + "," + mouse[1];
return d;
});
d3.selectAll(".mouse-per-line").attr("transform", function(d, i) {
var yDepth = y.invert(mouse[1]);
var bisect = d3.bisector(d => d.depth).right;
var idy = bisect(d.values, yDepth);
var beginning = 0;
var end = lines[i].getTotalLength();
var target = null;
while (true) {
target = Math.floor((beginning + end) / 2);
var pos = lines[i].getPointAtLength(target);
if (
(target === end || target === beginning) &&
pos.y !== mouse[1]
) {
break;
}
if (pos.y > mouse[1]) {
end = target;
} else if (pos.y < mouse[1]) {
beginning = target;
} else {
break;
}
}
d3.select(this)
.select("text")
.text(x.invert(pos.x).toFixed(2));
return "translate(" + pos.x + "," + mouse[1] + ")";
});
});
Fully working codesandbox here.
I'm using d3.js to plot a highway network over a map SVG. I'd like to be able to vary the stroke-weight of the line to illustrate demand based on a value.
Highway links are define as one way, so for example a two way road would have two overlapping line elements (with separate id's). I can use stroke-weight to edit the thickness of the line based on a variable (as below), but on a two way road, the larger of the two stroke weights will always cover the smaller rendering it invisible.
Is there an easy way to offset a line by half its stroke-weight to the left hand side of the direction the line is drawn? (direction denoted by x1,y1 x2,y2)
d3.csv("links.csv", function (error, data) {
d3.select("#lines").selectAll("line")
.data(data)
.enter()
.append("line")
.each(function (d) {
d.p1 = projection([d.lng1, d.lat1]);
d.p2 = projection([d.lng2, d.lat2]);
})
.attr("x1", function (d) { return d.p1[0]; })
.attr("y1", function (d) { return d.p1[1]; })
.attr("x2", function (d) { return d.p2[0]; })
.attr("y2", function (d) { return d.p2[1]; })
.on('mouseover', tip_link.show)
.on('mouseout', tip_link.hide)
.style("stroke", "black")
.style("stroke-width", lineweight)
});
One option would be to just create new start/end points when drawing your lines and use those:
var offset = function(start,destination,distance) {
// find angle of line
var dx = destination[0] - start[0];
var dy = destination[1] - start[1];
var angle = Math.atan2(dy,dx);
// offset them:
var newStart = [
start[0] + Math.sin(angle-Math.PI)*distance,
start[1] + Math.cos(angle)*distance
];
var newDestination = [
destination[0] + Math.sin(angle-Math.PI)*distance,
destination[1] + Math.cos(angle)*distance
];
// return the new start/end points
return [newStart,newDestination]
}
This function takes two points and offsets them by a particular amount given the angle between the two points. Negative values shift to the other side, swapping the start and destination points will shift to the other side.
In action, this looks like, with the original line in black:
var offset = function(start,destination,distance) {
// find angle of line
var dx = destination[0] - start[0];
var dy = destination[1] - start[1];
var angle = Math.atan2(dy,dx);
// offset them:
var newStart = [
start[0] + Math.sin(angle-Math.PI)*distance,
start[1] + Math.cos(angle)*distance
];
var newDestination = [
destination[0] + Math.sin(angle-Math.PI)*distance,
destination[1] + Math.cos(angle)*distance
];
// return the new start/end points
return [newStart,newDestination]
}
var line = [
[10,10],
[200,100]
];
var svg = d3.select("svg");
// To avoid repetition:
function draw(selection) {
selection.attr("x1",function(d) { return d[0][0]; })
.attr("x2",function(d) { return d[1][0]; })
.attr("y1",function(d) { return d[0][1]; })
.attr("y2",function(d) { return d[1][1]; })
}
svg.append("line")
.datum(line)
.call(draw)
.attr("stroke","black")
.attr("stroke-width",1)
svg.append("line")
.datum(offset(...line,6))
.call(draw)
.attr("stroke","orange")
.attr("stroke-width",10)
svg.append("line")
.datum(offset(...line,-4))
.call(draw)
.attr("stroke","steelblue")
.attr("stroke-width",5)
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
You will need to adapt this to your data structure, and it requires twice as many lines as before, because you aren't using stroke width, your using lines. This is advantageous if you wanted to use canvas.
My problem is simple to explain but I am having real trouble implementing a solution. I am trying to animate a circle along a path on a D3 map. The twist here is that I would like to use one of Mike Bostock's spinny globes (i.e. 3D map).
In time, I would like to add other paths to the globe and to use these for my animations. For now, I would simply like to animate the circles along the border of Russia (i.e. along the path of the Russia polygon coordinates)
I have built a jsfiddle to get traction on this and you can see all my code. Unfortunately I cannot get it to work, and am hoping you can help me out. My jsfiddle: http://jsfiddle.net/Guill84/xqmevpjg/7/
I think my key difficulty is (a) actually referencing the Russia path, and I think I am not getting it right at the moment, and (b) making sure that the interpolation is calculated properly (i.e. that the animation is dynamically linked to the globe, and not just 'layered on top'). The code that is supposed to do that is as follows:
setTimeout(function(){
var path = d3.select("path#Russia"),
startPoint = pathStartPoint(path);
marker.attr("r", 7)
.attr("transform", "translate(" + startPoint + ")");
transition();
//Get path start point for placing marker
function pathStartPoint(path) {
var d = path.attr("d"),
dsplitted = d.split(" ");
return dsplitted[1].split(",");
}
function transition() {
marker.transition()
.duration(7500)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);// infinite loop
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}
I'd be hugely grateful for any help.
For the first part of your question, one way to select the path is to add an id to id :
d3.json("http://mbostock.github.io/d3/talk/20111018/world-countries.json", function(collection) {
feature = svg.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.properties.name; }) ;
and then select the path like that :
var path = d3.select("#Russia").node()
Then you can select the first point with :
path.getPointAtLength(0)
See this updated fiddle : http://jsfiddle.net/xqmevpjg/11/
I searched for some help on building linear regression and found some examples here:
nonlinear regression function
and also some js libraries that should cover this, but unfortunately I wasn't able to make them work properly:
simple-statistics.js and this one: regression.js
With regression.js I was able to get the m and b values for the line, so I could use y = m*x + b to plot the line that followed the linear regression of my graph, but couldn't apply those values to the line generator, the code I tried is the following:
d3.csv("typeStatsTom.csv", function (error, dataset) {
//Here I plot other stuff, setup the x & y scale correctly etc.
//Then to plot the line:
var data = [x.domain(), y.domain()];
var result = regression('linear', data);
console.log(result)
console.log(result.equation[0]);
var linereg = d3.svg.line()
.x(function (d) { return x(d.Ascendenti); })
.y(function (d) { return y((result.equation[0] * d.Ascendenti) + result.equation[1]); });
var reglinepath = svg.append("path")
.attr("class", "line")
.attr("d", linereg(dataset))
.attr("fill", "none")
.attr("stroke", "#386cb0")
.attr("stroke-width", 1 + "px");
The values of result are the following in the console:
Object
equation: Array[2]
0: 1.8909425770308126
1: 0.042557422969139225
length: 2
__proto__: Array[0]
points: Array[2]
string: "y = 1.89x + 0.04"
__proto__: Object
From what I can tell in the console I should have set up the x and y values correctly, but of course the path in the resulting svg is not shown (but drawn), so I don't know what to do anymore. Any help is really really appreciated, even a solution involving the simple.statistics.js library would be helpful! Thanks!
I made it work using the following code found here:
function linearRegression(y,x){
var lr = {};
var n = y.length;
var sum_x = 0;
var sum_y = 0;
var sum_xy = 0;
var sum_xx = 0;
var sum_yy = 0;
for (var i = 0; i < y.length; i++) {
sum_x += x[i];
sum_y += y[i];
sum_xy += (x[i]*y[i]);
sum_xx += (x[i]*x[i]);
sum_yy += (y[i]*y[i]);
}
lr['slope'] = (n * sum_xy - sum_x * sum_y) / (n*sum_xx - sum_x * sum_x);
lr['intercept'] = (sum_y - lr.slope * sum_x)/n;
lr['r2'] = Math.pow((n*sum_xy - sum_x*sum_y)/Math.sqrt((n*sum_xx-sum_x*sum_x)*(n*sum_yy-sum_y*sum_y)),2);
return lr;
};
var yval = dataset.map(function (d) { return parseFloat(d.xHeight); });
var xval = dataset.map(function (d) { return parseFloat(d.Ascendenti); });
var lr = linearRegression(yval,xval);
// now you have:
// lr.slope
// lr.intercept
// lr.r2
console.log(lr);
And then plotting a line with:
var max = d3.max(dataset, function (d) { return d.OvershootingSuperiore; });
var myLine = svg.append("svg:line")
.attr("x1", x(0))
.attr("y1", y(lr.intercept))
.attr("x2", x(max))
.attr("y2", y( (max * lr.slope) + lr.intercept ))
.style("stroke", "black");
Using the code I found here
It looks to me like your path is getting drawn, just way off the screen.
Perhaps the regression is calculated incorrectly? The problem may be on line 202:
var data = [x.domain(), y.domain()];
var result = regression('linear', data);
If the raw data looks like [[1, 500], [2, 300]] this will find the linear regression of [[1, 2], [300, 500] which probably isn't what you want.
I'm guessing what you'd like to do is compute the regression with the entire set of data points rather than with the graph's bounds. Then rather than charting this line for every data value, you want to just plot the endpoints.
I'm using D3's world-countries.json file to create a mercator map of world countries, which I'll then bind to some data for a non-contiguous cartogram. Alas, the much larger sizes of Canada, the U.S., Australia, etc. mean that one unit for those countries is the spatial equivalent of several units for, say, Malta.
What I think I need to do is normalize the geojson shapes, such that Canada and Malta are the same size when starting out.
Any idea how I'd do that?
Thanks!
Update: I've tried explicitly setting the width and height of all the paths to a small integer, but that seems to just get overridden by the transform later. Code follows:
// Our projection.
var xy = d3.geo.mercator(),
path = d3.geo.path().projection(xy);
var states = d3.select("body")
.append("svg")
.append("g")
.attr("id", "states");
function by_number() {
function compute_by_number(collection, countries) {
//update
var shapes = states
.selectAll("path")
.data(collection.features, function(d){ return d.properties.name; });
//enter
shapes.enter().append("path")
.attr("d", path)
.attr("width", 5) //Trying to set width here; seems to have no effect.
.attr("height", 5) //Trying to set height here; seems to have no effect.
.attr("transform", function(d) { //This works.
var centroid = path.centroid(d),
x = centroid[0],
y = centroid[1];
return "translate(" + x + "," + y + ")"
+ "scale(" + Math.sqrt(countries[d.properties.name] || 0) + ")"
+ "translate(" + -x + "," + -y + ")";
})
.append("title")
.text(function(d) { return d.properties.name; });
//exit
}
d3.text("../data/country_totals.csv", function(csvtext){
var data = d3.csv.parse(csvtext);
var countries = [];
for (var i = 0; i < data.length; i++) {
var countryName = data[i].country.charAt(0).toUpperCase() + data[i].country.slice(1).toLowerCase();
countries[countryName] = data[i].total;
}
if (typeof window.country_json === "undefined") {
d3.json("../data/world-countries.json", function(collection) {
window.country_json = collection;
compute_by_number(collection, countries);
});
} else {
collection = window.country_json;
compute_by_number(collection, countries);
}
});
} //end by_number
by_number();
You might be able to use the helper function I posted here: https://gist.github.com/1756257
This scales a projection to fit a given GeoJSON object into a given bounding box. One advantage of scaling the projection, rather than using a transform to scale the whole path, is that strokes can be consistent across maps.
Another, simpler option might be to:
Project the paths;
Use path.getBBox() to get the bounding box for each (.getBBox() is a native SVG function, not a D3 method)
Set a transform on the path, similar to how you do it now, to scale and translate the path to fit your bounding box.
This is a bit simpler, as it doesn't involve projections, but you'll need to scale the stroke by the inverse (1/scale) to keep them consistent (and therefore you won't be able to set stroke values with CSS). It also requires actually rendering the path first, then scaling it - this might affect performance for complex geometries.