Skip to content

Commit 1315d07

Browse files
committed
fix point distance calculation; it's more accurate now and covers the case where all points are in a horizontal or vertical line
1 parent 42cfce1 commit 1315d07

1 file changed

Lines changed: 46 additions & 22 deletions

File tree

src/traces/quiver/calc.js

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ var BADNUM = require('../../constants/numerical').BADNUM;
77
var colorscaleCalc = require('../../components/colorscale/calc');
88
var calcSelection = require('../scatter/calc_selection');
99

10-
/**
11-
* Main calculation function for quiver trace
12-
* Creates calcdata with arrow path data for each vector
13-
*/
10+
// For scaled lengthmode: Constant to multiply by the computed distance between
11+
// neighboring points, such that the arrows are _just slightly shorter_ than
12+
// that distance
13+
const SHRINK_FACTOR = 0.97;
14+
// const SHRINK_FACTOR = 1;
15+
1416
module.exports = function calc(gd, trace) {
1517
// Map x/y through axes so category/date values become numeric calcdata
1618
const xa = trace._xA = Axes.getFromId(gd, trace.xaxis || 'x', 'x');
@@ -107,27 +109,49 @@ module.exports = function calc(gd, trace) {
107109
// Store maxNorm for use by plot step
108110
trace._maxNorm = normMax;
109111

112+
// Ignore lengthmode 'raw' if arrowref is set to 'paper': always scale
110113
if (lengthmode === 'scaled' || arrowref === 'paper') {
111-
// Ignore lengthmode 'raw' if arrowref is set to 'paper': always scale
112-
113-
// Compute point density of the entire trace: Area of bounding box
114-
// divided by number of points. This is used to scale arrows in
115-
// 'scaled' lengthmode.
116-
// TODO: How to handle the case where there is just one point in a trace,
117-
// or all points have the same x or y value? This will give a boxArea of 0.
118-
// For now I'm going to just normalize to a vector of unit length (1) in that case,
119-
// but that's not a great solution
120-
const boxArea = (xMax - xMin) * (yMax - yMin);
121-
const pointDensity = boxArea / len;
122-
// Now, compute the scale factor for scaled lengthmode
123-
// The scale factor should be such that
124-
// _maxNorm * _scaleFactor = Math.sqrt(_pointDensity)
125-
// Therefore: _scaleFactor = Math.sqrt(_pointDensity) / _maxNorm
126-
if (pointDensity === 0) {
127-
trace._scaleFactor = 1 / trace._maxNorm
114+
/**
115+
* Compute the maximum arrow length we should allow, using a heuristic
116+
* to estimate the distance between neighboring points.
117+
*
118+
* Let:
119+
* - D be the distance between neighboring points (the value we want to compute)
120+
* - N be the number of points in the trace
121+
* - dX be the x-width of the bounding box of all the points
122+
* - dY be the y-width of the bounding box
123+
*
124+
* We want to satisfy this equation: D = sqrt((dX + D) * (dY + D) / N)
125+
*
126+
* This is basically the square root of the point density, with an additional
127+
* adjustment to account for the points on the edges (we add D to each dimension
128+
* of the bounding box). This equation gives us the _exact_ correct distance when
129+
* the points are arranged in a perfect grid; otherwise, it's just an estimate.
130+
*
131+
* Solving for D gives us:
132+
* D = (dX + dY + sqrt((dX - dY)^2 + 4N * dX * dY)) / (2 * (N - 1))
133+
* which is the forumla we'll use below.
134+
*
135+
* Note: this formula was derived and documented by a human ;)
136+
*/
137+
138+
const dX = xMax - xMin;
139+
const dY = yMax - yMin;
140+
var pointDist;
141+
if (dX === 0 && dY === 0) {
142+
// If all points share the same x and y value, we can't estimate pointDist.
143+
// Default to an arbitrary value of 1.
144+
pointDist = 1;
128145
} else {
129-
trace._scaleFactor = Math.sqrt(pointDensity) / trace._maxNorm;
146+
// Use the formula derived above
147+
pointDist = (dX + dY + Math.sqrt((dX - dY) * (dX - dY) + 4 * nValid * dX * dY)) / (2 * (nValid - 1));
130148
}
149+
pointDist *= SHRINK_FACTOR; // Adjust to slightly less than the computed distance
150+
151+
// Set the trace scale factor such that the longest vector will have
152+
// a length equal to the computed pointDist
153+
trace._scaleFactor = pointDist / trace._maxNorm;
154+
131155
// Note: If arrowref === 'paper', this scale factor must be
132156
// multiplied by Math.sqrt(xa._m * ya._m), but we can't do that quite yet
133157
// since the axis scales are not fully determined. Do it in plot step instead.

0 commit comments

Comments
 (0)