How to write a d3 positive and negative logarithmic scale - javascript

Question
I have positive and negative values, and I'd like to plot them on a "logarithmic" scale.
Imagine a scale with evenly spaced ticks for the following values:
-1000, -100, -10, -1, 0, 1, 10, 100, 1000
I want 0 in there, which is defined to be -Inf by logarithms, complicating this further.
However, I don't think this request is unreasonable. This seems like a sensible scale any data scientist might want to plot strongly divergent values against.
How do you create such a scale and axis in d3?
Thoughts
It might be possible to do this cleverly with 2 d3.scaleLog()s or maybe 3 scales if you use a technique like this one.
I was hoping there might be an easy way to fit this in a d3.scalePow() with .exponent(0.1) but unless I've got my log rules mixed up, you can't get a .scaleLog() out of a .scalePow() (though you can probably approximate it okay for some ranges).

We can't have a true log scale like this, or even a combination of two log scales like this as. We need to set a cut off for zeroish values, and this is where error might be introduced depending on your data. Otherwise, to make a scale function like this is fairly straightforward, just call a different scale for negative and positive while setting zero-ish values to zero.
This combination of scales might look like:
var positive = d3.scaleLog()
.domain([1e-6,1000])
.range([height/2,0])
var negative = d3.scaleLog()
.domain([-1000,-1e-6])
.range([height,height/2])
var scale = function(x) {
if (x > 1e-6) return positive(x);
else if (x < -1e-6) return negative(x);
else return height/2; // zero value.
}
And an example:
var width = 500;
var height = 300;
var positive = d3.scaleLog()
.domain([1e-1,1000])
.range([height/2,0])
var negative = d3.scaleLog()
.domain([-1000,-1e-1])
.range([height,height/2])
var scale = function(x) {
if (x > 1e-6) return positive(x);
else if (x < -1e-6) return negative(x);
else return height/2; // zero value.
}
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
svg.append("path")
.attr("d", line(data) );
path {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Creating a single scale
The above is a proof of concept maybe.
Now the trickier part is making an axis. We could make axes for both the scales above, leaving zero with some sort of manual correction. But it will be easier to create a scale with our own interpolotor using the above as an exmaple. This gives us one scale which we can create an axis for. Our interpolator might look like:
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.0001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) return positive(t);
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2;
}
}
And then we can apply that to a regular old d3 linear scale:
d3.scaleLinear().interpolate(interpolator)...
This will interpolate numbers in the domain to the range as we've specified. It largely takes the above and adopts it for use as a d3 interpolator: a,b are the domain limits, t is a normalized domain between 0 and 1, and k defines the zeroish values. More on k below.
To get the ticks, assuming an nice round domain that only has nice round base ten numbers we could use:
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 1) {
ticks.push(d); d /= 10;
}
})
Applying this we get:
var margin = {left: 40, top: 10, bottom: 10}
var width = 500;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width+margin.left)
.attr("height",height+margin.top+margin.bottom)
.append("g").attr("transform","translate("+[margin.left,margin.top]+")");
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.0001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) return positive(t);
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2;
}
}
// Create a scale using it:
var scale = d3.scaleLinear()
.range([height,0])
.domain([-1000,1000])
.interpolate(interpolator);
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 1) {
ticks.push(d); d /= 10;
}
})
// Apply the scale:
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
// Draw a line:
svg.append("path")
.attr("d", line(data) )
.attr("class","line");
// Add an axis:
var axis = d3.axisLeft()
.scale(scale)
.tickValues(ticks)
svg.append("g").call(axis);
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Modifying the k value
Ok, what's up about k. It is needed to set the zeroish values. k also changes the shape of the graph. If showing regularly spaced ticks, increasing k ten fold increases the magnitude of the minimum magnitude ticks (other than zero) ten fold. In my exmaples above multiplying k by ten pushes the ticks with magnitude one overtop of the zero tick. Dividing it by ten would create room for a 0.1 tick (of course that requires modifying the tick generator to show that tick). k is hard to explain so I hope I did ok there.
I'll demonstrate to try and communicate it a bit better. Let's set the minimum magnitude ticks to be 0.1 using the above, we'll want to modify the tick function and k:
var margin = {left: 40, top: 10, bottom: 10}
var width = 500;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width+margin.left)
.attr("height",height+margin.top+margin.bottom)
.append("g").attr("transform","translate("+[margin.left,margin.top]+")");
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.00001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) {return positive(t)};
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2 //yd;
}
}
// Create a scale using it:
var scale = d3.scaleLinear()
.range([height,0])
.domain([-1000,1000])
.interpolate(interpolator);
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 0.1) {
ticks.push(d); d /= 10;
}
})
// Apply the scale:
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
// Draw a line:
svg.append("path")
.attr("d", line(data) )
.attr("class","line");
// Add an axis:
var axis = d3.axisLeft()
.scale(scale)
.tickValues(ticks)
.ticks(10,".1f")
svg.append("g").call(axis);
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
If you have a domain of +/- 1000, and you want the minimum magnitude tick to be 1 (not including zero) you want a k of 0.0001, or 0.1/1000.
If positive and negative limits of the domain are different, then we would need two k values, one for the negative cut off and one for the positive.
Lastly,
k sets values that are zeroish, in my example, t values that are between -k and +k are set to be the same - 0. Ideally this won't be many values in the dataset, but if it is, you might get a line such as:
Each input value is different, but the there are many zero output values, producing a visual artifact due to the bounds of what I haved considered to be zeroish. If there is only one value in the zeroish bounds, like in my examples above (but not the above picture), we get a much nicer:

Related

How do I set boundaries for my bubbles and trigger movement on-command?

I have a bubble chart in D3 and I'm using it to show how many bubbles there are per group. This version has about 500 bubbles to start with and my full version has about 3,000.
I am struggling along two dimension. I'm trying to get the bubbles to stay put when they're not transitioning between states and I'm also trying to get the bubbles to create a rectangular shape.
This is a demo of the bubble chart. I'll add the code and then go through what I've tried.
This is the code for my bubbles.
// Initial time and quarter
let time_so_far = 0;
let quarter = 0;
const tick_time = 100
// Forces
const radius = 1.5
const padding1 = 10;
const padding2 = 2;
const strength = 50
const veloc_decay = .99
const alpha = .05
const alpha_decay = 0
const alpha_min = 0.001
const alpha_Collision = .08;
const charge_strength = -.5
const charge_theta = .9
// Load data
Promise.all([
d3.tsv("stages.tsv", d3.autoType),
d3.tsv("customers.tsv", d3.autoType),
])
// Once data is loaded...
.then(function(files){
// Prepare the data...
const stage_data = files[0]
const customer_data = files[1]
// Consolidate stages by id.
stage_data.forEach(d => {
if (d3.keys(stakeholders).includes(d.id+"")) {
stakeholders[d.id+""].push(d);
} else {
stakeholders[d.id+""] = [d];
}
});
// Consolidate customers by week.
customer_data.forEach(d => {
if (d3.keys(customers).includes(d.week+"")) {
customers[d.week+""].push(d);
} else {
customers[d.week+""] = [d];
}
});
// Create node data.
var nodes = d3.keys(stakeholders).map(function(d) {
// Initialize count for each group.
groups[stakeholders[d][0].stage].cnt += 1;
return {
id: "node"+d,
x: groups[stakeholders[d][0].stage].x + Math.random(),
y: groups[stakeholders[d][0].stage].y + Math.random(),
r: radius,
color: groups[stakeholders[d][0].stage].color,
group: stakeholders[d][0].stage,
timeleft: stakeholders[d][0].weeks,
istage: 0,
stages: stakeholders[d]
}
});
// Circle for each node.
const circle = svg.append("g")
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("fill", d => d.color)
.attr("r", d => d.r);
// Forces
const simulation = d3.forceSimulation(nodes)
// .force("bounds", boxingForce)
.force("x", d => d3.forceX(d.x))
.force("y", d => d3.forceY(d.y))
.force("cluster", forceCluster())
.force("collide", forceCollide())
.force("charge", d3.forceManyBody().strength(charge_strength).theta(charge_theta))
// .force('center', d3.forceCenter(center_x, center_y))
.alpha(alpha)
.alphaDecay(alpha_decay)
.alphaMin(alpha_min)
.velocityDecay(veloc_decay)
// Adjust position of circles.
simulation.on("tick", () => {
circle
.attr("cx", d => Math.max(r, Math.min(500 - r, d.x)))
.attr("cy", d => Math.max(r, Math.min(500 - r, d.y)))
.attr("fill", d => groups[d.group].color);
});
// Force to increment nodes to groups.
function forceCluster() {
let nodes;
function force(alpha) {
const l = alpha * strength;
for (const d of nodes) {
d.vx -= (d.x - groups[d.group].x) * l;
d.vy -= (d.y - groups[d.group].y) * l;
}
}
force.initialize = _ => nodes = _;
return force;
}
// Force for collision detection.
function forceCollide() {
let nodes;
let maxRadius;
function force() {
const quadtree = d3.quadtree(nodes, d => d.x, d => d.y);
for (const d of nodes) {
const r = d.r + maxRadius;
const nx1 = d.x - r, ny1 = d.y - r;
const nx2 = d.x + r, ny2 = d.y + r;
quadtree.visit((q, x1, y1, x2, y2) => {
if (!q.length) do {
if (q.data !== d) {
const r = d.r + q.data.r + (d.group === q.data.group ? padding1 : padding2);
let x = d.x - q.data.x, y = d.y - q.data.y, l = Math.hypot(x, y);
if (l < r) {
l = (l - r) / l * alpha_Collision;
d.x -= x *= l, d.y -= y *= l;
q.data.x += x, q.data.y += y;
}
}
} while (q = q.next);
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
}
}
force.initialize = _ => maxRadius = d3.max(nodes = _, d => d.r) + Math.max(padding1, padding2);
return force;
}
// Make time pass. Adjust node stage as necessary.
function timer() {
// Ticker...
nodes.forEach(function(o,i) {
o.timeleft -= 1;
if (o.timeleft == 0 && o.istage < o.stages.length-1) {
// Decrease counter for previous group.
groups[o.group].cnt -= 1;
// Update current node to new group.
o.istage += 1;
o.group = o.stages[o.istage].stage;
o.timeleft = o.stages[o.istage].weeks;
// Increment counter for new group.
groups[o.group].cnt += 1;
}
});
// Previous quarter
quarter = Math.floor(time_so_far / 12)
// Increment time.
time_so_far += 1;
// goes by week, timer updates every quarter
var current_quarter = Math.floor(time_so_far / 13) + 1
// stop on the last quarter
if(time_so_far == d3.keys(customers).length) { return }
d3.select("#timecount .cnt").text(quarters[current_quarter]);
// update counter
d3.selectAll(".counter")
.text(d => d.cnt)
// Define length of a tick
d3.timeout(timer, tick_time);
} // #end timer()
timer()
}); // end TSV
Right now, my bubbles are constantly moving. Even if I make the space for the bubbles really large and the padding really small, they keep moving.
I've tried to set .alphaDecay() to a value greater than 0 and it gets the bubbles to stop moving and they look quite good, but then they don't have energy to transition between states.
I'd like to set it so that the bubbles find their spot when the page loads and then they don't move, except to change from no interactions to portfolio to partner similar to the bubble chart here.
The other problem is that the bubbles congregate as circles. I'd like to get them to fill in the whole rectangular backdrop for each of the states.
Per Mike Bostock's comments, I added boundaries in the simulation.on function. It works to set boundaries on the whole space, but it doesn't apply the boundaries to each state individually, so they still end up clustering as circles.
I've also tried John Guerra's d3.forceBoundary but I run into the same problem.
How can I force the bubbles to stay in one position and only move when a transition in states occurs and how can I get the bubbles to congregate in rectangles on each state?
Edit: I tried to set alphaDecay > 0 so the bubbles would initialize and stop moving and then I added a new alpha value in the .on("tick", function, but that just let them keep energy.
The core of the problem is that I don't know how to apply force that lets them move across the viz from one state to another state, but doesn't cause them to jumble around.
My next try is going to be to create a different force for changing states than for getting created.
Edit2: I've got a solution going for the energy problem. It's a bit hacky.
I added o.statechange = 3 within the if loop inside nodes.forEach(function(o,i) { and I added o.statechange -= 1 right above the if loop. And then, in forceCluster I added
for (var i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {
node = nodes[i];
if(node.statechange <= 0) { continue }
node.vx -= (node.x - groups[node.group].x) * k;
node.vy -= (node.y - groups[node.group].y) * k;
}
This gives circles energy for three ticks if they need to make a move. Otherwise, they don't get any. (Last edit, this work-around worked for a small number of nodes but it fails as the number of nodes gets larger)

Split an SVG path lengthwise into multiple colours

I have a tree visualisation in which I am trying to display paths between nodes that represent a distribution with multiple classes. I want to split the path lengthwise into multiple colours to represent the frequency of each distribution.
For example: say we have Class A (red) and Class B (black), that each have a frequency of 50. Then I would like a path that is half red and half black between the nodes. The idea is to represent the relative frequencies of the classes, so the frequencies would be normalised.
My current (naive) attempt is to create a separate path for each class and then use an x-offset. It looks like this.
However, as shown in the image, the lines do not maintain an equal distance for the duration of the path.
The relevant segment of code:
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "red")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + -2.5 + "," + 0.0 + ")"; });
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "black")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + 2.5 + "," + 0.0 + ")"; });
It would be great if anyone has some advice.
Thanks!
A possible solution is to calculate the individual paths and fill with the required color.
Using the library svg-path-properties from geoexamples.com you can calculate properties (x,y,tangent) of a path without creating it first like it is done in this SO answer (this does not calculate the tangent).
The code snippet does it for 2 colors but it can be easy generalized for more.
You specify the colors, percentage and width of the stroke with a dictionary
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
percent is the amount color[0] takes from the stroke width.
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
The pathPoints parameters
path that needs to be stroked, can be generated by d3.line path example from SO answer
var lineGenerator = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveNatural);
var curvePoints = [[0,0],[0,10],[20,30]];
var duoPath = pathPoints(lineGenerator(curvePoints), 10, duoProp);
path length interval at which to sample (unit pixels). Every 10 pixels gives a good approximation
dictionary with the percent and width of the stroke
It returns an array with the paths to be filled, 1 for each color.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/svg-path-properties#0.4.4/build/path-properties.min.js"></script>
</head>
<body>
<svg id="chart" width="350" height="350"></svg>
<script>
var svg = d3.select("#chart");
function pathPoints(path, stepLength, duoProp) {
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(d => props.getPropertiesAtLength(d));
var pFactor = percent => (percent - 0.5) * duoProp.width;
tProps.forEach(p => {
p.x0 = p.x - pFactor(0) * p.tangentY;
p.y0 = p.y + pFactor(0) * p.tangentX;
p.xP = p.x - pFactor(duoProp.percent) * p.tangentY;
p.yP = p.y + pFactor(duoProp.percent) * p.tangentX;
p.x1 = p.x - pFactor(1) * p.tangentY;
p.y1 = p.y + pFactor(1) * p.tangentX;
});
var format1d = d3.format(".1f");
var createPath = (forward, backward) => {
var fp = tProps.map(p => forward(p));
var bp = tProps.map(p => backward(p));
bp.reverse();
return 'M' + fp.concat(bp).map(p => `${format1d(p[0])},${format1d(p[1])}`).join(' ') + 'z';
}
return [createPath(p => [p.x0, p.y0], p => [p.xP, p.yP]), createPath(p => [p.xP, p.yP], p => [p.x1, p.y1])]
}
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
</script>
</body>
</html>
As a quick follow-up to rioV8's excellent answer, I was able to get their code working but needed to generalise it to work with more than two colours. In case someone else has a similar requirement, here is the code:
function pathPoints(path, stepLength, duoProp) {
// get the properties of the path
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
// build a list of segments to use as approximation points
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(function (d) {
return props.getPropertiesAtLength(d);
});
// incorporate the percentage
var pFactor = function pFactor(percent) {
return (percent - 0.5) * duoProp.width;
};
// for each path segment, calculate offset points
tProps.forEach(function (p) {
// create array to store modified points
p.x_arr = [];
p.y_arr = [];
// calculate offset at 0%
p.x_arr.push(p.x - pFactor(0) * p.tangentY);
p.y_arr.push(p.y + pFactor(0) * p.tangentX);
// calculate offset at each specified percent
duoProp.percents.forEach(function(perc) {
p.x_arr.push(p.x - pFactor(perc) * p.tangentY);
p.y_arr.push(p.y + pFactor(perc) * p.tangentX);
});
// calculate offset at 100%
p.x_arr.push(p.x - pFactor(1) * p.tangentY);
p.y_arr.push(p.y + pFactor(1) * p.tangentX);
});
var format1d = d3.format(".1f");
var createPath = function createPath(forward, backward) {
var fp = tProps.map(function (p) {
return forward(p);
});
var bp = tProps.map(function (p) {
return backward(p);
});
bp.reverse();
return 'M' + fp.concat(bp).map(function (p) {
return format1d(p[0]) + "," + format1d(p[1]);
}).join(' ') + 'z';
};
// create a path for each projected point
var paths = [];
for(var i=0; i <= duoProp.percents.length; i++) {
paths.push(createPath(function (p) { return [p.x_arr[i], p.y_arr[i]]; }, function (p) { return [p.x_arr[i+1], p.y_arr[i+1]]; }));
}
return paths;
}
// generate the line
var duoProp = { color: ["red", "blue", "green"], percents: [0.5, 0.7], width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
Note that the percents array specifies the cumulative percentage of the stroke, not the individual percentages of the width. E.g. in the example above, the red stroke will span 0% to 50% width, the blue stroke 50% to 70% width and the green stroke 70% to 100% width.

filtering data and working with scales - d3js

I have a simple scatterplot in d3js. The aim of the visualization is to fade out points on a selection. This works. Congruent with this, a new trendline should appear only for those selected points as well as an updated slope equation and R2 value. The fading of points and updating of slope equation/R2 values is working on selection. However, the trendline appears to be truncated and not scaled correctly, but I can't figure out why.
Here is a working version.
Following the on.change the following code is executed:
filteredData = filterJSON(data, 'name', value); // gets filtered json data
var x = d3.scaleLinear()
.range([0,width]);
var y = d3.scaleLinear()
.range([height,0]);
var xSeries1 = filteredData.map(function(e) { return e.x; }); // new x values
var ySeries1 = filteredData.map(function(e) { return e.y; }); // new y values
var rsq1 = leastSquares(xSeries1,ySeries1); // calculates r2/slope etc. - see function below
// Add trendline
ptAx1 = d3.min(xSeries1);
ptAy1 = rsq1[0] * d3.min(xSeries1) + rsq1[1];
ptBy1 = d3.min(ySeries1);
ptBx1 = (d3.min(ySeries1) - rsq1[1]) / rsq1[0];
svg.append("line")
.attr("class", "regression")
.attr("x1", x(ptAx1))
.attr("y1", y(ptAy1))
.attr("x2", x(ptBx1))
.attr("y2", y(ptBy1));
// calculate linear regression
function leastSquares(xSeries,ySeries) {
var reduceSumFunc = function(prev, cur) { return prev + cur; };
var xBar = xSeries.reduce(reduceSumFunc) * 1.0 / xSeries.length;
var yBar = ySeries.reduce(reduceSumFunc) * 1.0 / ySeries.length;
var ssXX = xSeries.map(function(d) { return Math.pow(d - xBar, 2); })
.reduce(reduceSumFunc);
var ssYY = ySeries.map(function(d) { return Math.pow(d - yBar, 2); })
.reduce(reduceSumFunc);
var ssXY = xSeries.map(function(d, i) { return (d - xBar) * (ySeries[i] - yBar); })
.reduce(reduceSumFunc);
var slope = ssXY / ssXX;
var intercept = yBar - (xBar * slope);
var rSquare = Math.pow(ssXY, 2) / (ssXX * ssYY);
return [slope, intercept, rSquare];
}
This code works well when all data points (no filtering of data), but doesn't when filtering occurs.
This is all points - trendline ok
This is filtered points - trendline truncated
It looks like you left "min" where you meant "max" in assigning values to ptBy1 and ptBx1
Made this change in your "blockbuilder" and it seemed to work as intended.

How to calculate y value for given x using d3 line generator? [duplicate]

I display a line chart with D3 with roughly the following code (given the scale functions x, y and the float array data):
var line = d3.svg.line()
.interpolate("basis")
.x(function (d, i) { return x(i); })
.y(function (d) { return y(d); });
d3.select('.line').attr('d', line(data));
Now I want to know the vertical height of the line at a given horizontal pixel position. The data array has lesser data points than pixels and the displayed line is interpolated, so it is not straight-forward to deduce the height of the line at a given pixel just from the data array.
Any hints?
This solution is much more efficient than the accepted answer. It's execution time is logarithmic (while accepted answer has linear complexity).
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
}
Edited 19-Sep-2012 per comments with many thanks to nrabinowitz!
You will need to do some sort of search of the data returned by getPointAtLength. (See https://developer.mozilla.org/en-US/docs/DOM/SVGPathElement.)
// Line
var line = d3.svg.line()
.interpolate("basis")
.x(function (d) { return i; })
.y(function(d, i) { return 100*Math.sin(i) + 100; });
// Append the path to the DOM
d3.select("svg#chart") //or whatever your SVG container is
.append("svg:path")
.attr("d", line([0,10,20,30,40,50,60,70,80,90,100]))
.attr("id", "myline");
// Get the coordinates
function findYatX(x, linePath) {
function getXY(len) {
var point = linePath.getPointAtLength(len);
return [point.x, point.y];
}
var curlen = 0;
while (getXY(curlen)[0] < x) { curlen += 0.01; }
return getXY(curlen);
}
console.log(findYatX(5, document.getElementById("myline")));
For me this returns [5.000403881072998, 140.6229248046875].
This search function, findYatX, is far from efficient (runs in O(n) time), but illustrates the point.
I have tried implementing findYatXbisection (as nicely suggested by bumbu), and I could not get it to work AS IS.
Instead of modifying the length as a function of length_end and length_start, I just decreased the length by 50% (if x < point.x) or increased by 50% (if x> point.x) but always relative to start length of zero. I have also incorporated revXscale/revYscale to convert pixels to x/y values as set by my d3.scale functions.
function findYatX(x,path,error){
var length = apath.getTotalLength()
, point = path.getPointAtLength(length)
, bisection_iterations_max=50
, bisection_iterations = 0
error = error || 0.1
while (x < revXscale(point.x) -error || x> revXscale(point.x + error) {
point = path.getPointAtlength(length)
if (x < revXscale(point.x)) {
length = length/2
} else {
length = 3/2*length
}
if (bisection_iterations_max < ++ bisection_iterations) {
break;
}
}
return revYscale(point.y)
}

In d3, how to get the interpolated line data from a SVG line?

I display a line chart with D3 with roughly the following code (given the scale functions x, y and the float array data):
var line = d3.svg.line()
.interpolate("basis")
.x(function (d, i) { return x(i); })
.y(function (d) { return y(d); });
d3.select('.line').attr('d', line(data));
Now I want to know the vertical height of the line at a given horizontal pixel position. The data array has lesser data points than pixels and the displayed line is interpolated, so it is not straight-forward to deduce the height of the line at a given pixel just from the data array.
Any hints?
This solution is much more efficient than the accepted answer. It's execution time is logarithmic (while accepted answer has linear complexity).
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
}
Edited 19-Sep-2012 per comments with many thanks to nrabinowitz!
You will need to do some sort of search of the data returned by getPointAtLength. (See https://developer.mozilla.org/en-US/docs/DOM/SVGPathElement.)
// Line
var line = d3.svg.line()
.interpolate("basis")
.x(function (d) { return i; })
.y(function(d, i) { return 100*Math.sin(i) + 100; });
// Append the path to the DOM
d3.select("svg#chart") //or whatever your SVG container is
.append("svg:path")
.attr("d", line([0,10,20,30,40,50,60,70,80,90,100]))
.attr("id", "myline");
// Get the coordinates
function findYatX(x, linePath) {
function getXY(len) {
var point = linePath.getPointAtLength(len);
return [point.x, point.y];
}
var curlen = 0;
while (getXY(curlen)[0] < x) { curlen += 0.01; }
return getXY(curlen);
}
console.log(findYatX(5, document.getElementById("myline")));
For me this returns [5.000403881072998, 140.6229248046875].
This search function, findYatX, is far from efficient (runs in O(n) time), but illustrates the point.
I have tried implementing findYatXbisection (as nicely suggested by bumbu), and I could not get it to work AS IS.
Instead of modifying the length as a function of length_end and length_start, I just decreased the length by 50% (if x < point.x) or increased by 50% (if x> point.x) but always relative to start length of zero. I have also incorporated revXscale/revYscale to convert pixels to x/y values as set by my d3.scale functions.
function findYatX(x,path,error){
var length = apath.getTotalLength()
, point = path.getPointAtLength(length)
, bisection_iterations_max=50
, bisection_iterations = 0
error = error || 0.1
while (x < revXscale(point.x) -error || x> revXscale(point.x + error) {
point = path.getPointAtlength(length)
if (x < revXscale(point.x)) {
length = length/2
} else {
length = 3/2*length
}
if (bisection_iterations_max < ++ bisection_iterations) {
break;
}
}
return revYscale(point.y)
}

Categories

Resources