@@ -7,10 +7,12 @@ var BADNUM = require('../../constants/numerical').BADNUM;
77var colorscaleCalc = require ( '../../components/colorscale/calc' ) ;
88var 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+
1416module . 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