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.
Related
So I have a D3 SVG bezier curve like this :
I want to get X, Y coordinates of points with given step from the D3 path like so:
So to get an array of coordinate pairs with equal steps. If possible not recalculating bezier curve. How to do such a thing in D3js?
With the help of a bisection function from here you probably want something like this:
const
svg = d3.select("svg"),
width = 512,
height = 200;
const data = [];
const curve = "curveBasis";
var walk = function() {
for (let i = 0, v = 2; i < 50; ++i) {
v += Math.random() - 0.5;
v = Math.max(Math.min(v, 4), 0);
data.push({step: i, value: v});
}
}
walk();
walkX = d3.scaleLinear()
.domain([0, 49])
.range([10, width - 10]);
walkY = d3.scaleLinear()
.domain([0, 4])
.range([200 - 10, 10]);
const line = d3.line()
.curve(d3[curve])
.x(d => walkX(d.step))
.y(d => walkY(d.value));
svg
.append("path")
.attr("id", "svgpath")
.attr("d", line(data))
.attr("fill", "none")
.attr("stroke", "black");
var svgpath = document.getElementById("svgpath");
var findYatXbyBisection = function(x, path, error){
var length_end = path.getTotalLength(),
length_start = 0,
point = path.getPointAtLength((length_end + length_start) / 2), // get the middle point
bisection_iterations_max = 50,
bisection_iterations = 0;
error = error || 0.01;
while (x < point.x - error || x > point.x + error) {
// get the middle point
point = path.getPointAtLength((length_end + length_start) / 2);
if (x < point.x) {
length_end = (length_start + length_end)/2;
} else {
length_start = (length_start + length_end)/2;
}
// Increase iteration
if (bisection_iterations_max < ++ bisection_iterations)
break;
}
return point.y
}
for (let i = 0; i < data.length; ++i) {
console.log(findYatXbyBisection(walkX(i), svgpath, 0.01).toFixed(4));
}
<html>
<head>
<meta charset="utf-8" />
<title>SVG Line</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.1/d3.min.js" integrity="sha512-COTaPOlz12cG4fSfcBsxZsjauBAyldqp+8FQUM/dZHm+ts/jR4AFoJhCqxy8K10Jrf3pojfsbq7fAPTb1XaVkg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<svg id="chart" width="512" height="200">
</svg>
Do note that Y values returned are SVG coordinates so they start at 0 from the top of page. Check the range function used in the walkY function to refresh how you have to reverse the values for typical line and bar charts in D3.js.
And of course instead of logging to the console you can push the values to your custom array and use different interval value e.g. 1/n of the total line (path) width instead of 1/50 I've used (for 50 data points).
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.
I have a situation very similar to the one in this JSFiddle with some points representing a team (in particular its final rank in a football season).
I would like to substitute the points with a line passing exactly in these points so that the final result shows the temporal evolution of each team in terms of final ranking position.
I know how to create a line by setting the X1,X2,Y1,Y2 coordinates but I don't understand how to set this coordinates to the exact value (e.g. if the line is between season 2006-2007 and season 2007-2008 I will have to set X1 and Y1 with value from the first season as d[0] and d[1] but for X2 and Y2 I need values from the next element in the array.
I'm very new with D3.js so any advice and solution is very welcome. Thanks
Assuming you have already declared some data for your lines drawing the actual lines based on that data is as simple as this:
create the X and Y scales:
var xScale = d3.scale.linear().domain([dataRange.x1, dataRange.x2]).range([plotRange.x1, plotRange.x2]);
var yScale = d3.scale.linear().domain([dataRange.y1, dataRange.y2]).range([plotRange.y1, plotRange.y2]);
declare the line function:
var valueLine = d3.svg.line()
.x(function (dataItem, arrayIndex) {
return xScale(dataItem);
})
.y(function (dataItem, arrayIndex) {
return yScale(dataItem)
});
and finally create the path:
g.append("path")
.style("stroke", someColour)
.attr("d", valueLine(myData))
.attr("class", "someClass");
Refer to more documentation here: https://www.dashingd3js.com/
Based on that fiddle, this is what I'd do:
First, I'd set a class to each team's circles (team1, team2 and so on...). So, I could later retrieve the circles' values for each team.
For retrieving the circles values, I'd use a for loop:
for(var j = 1; j < 4; j++){//this loops from "Team1" to "Team3"
var team = d3.selectAll("circle.Team" + j)[0];//selects the team by class
for(var i = 0; i < team.length; i++){//this loops through the circles
if(team[i+1]){//if the next circle exists
svg.append("line")
.attr("x1", d3.select(team[i]).attr("cx"))//this circle
.attr("y1", d3.select(team[i]).attr("cy"))//this circle
.attr("x2", d3.select(team[i+1]).attr("cx"))//the next circle
.attr("y2", d3.select(team[i+1]).attr("cy"))//the next circle
.attr("stroke", function(){
return _TEAM_COLORS_["Team" + j]
});//sets the colours based on your object
}
}
};
Here is that fiddle, updated: https://jsfiddle.net/gerardofurtado/6cc0ehz2/18/
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.