@@ -13,34 +13,56 @@ var calcSelection = require('../scatter/calc_selection');
1313 */
1414module . exports = function calc ( gd , trace ) {
1515 // Map x/y through axes so category/date values become numeric calcdata
16- var xa = trace . _xA = Axes . getFromId ( gd , trace . xaxis || 'x' , 'x' ) ;
17- var ya = trace . _yA = Axes . getFromId ( gd , trace . yaxis || 'y' , 'y' ) ;
16+ const xa = trace . _xA = Axes . getFromId ( gd , trace . xaxis || 'x' , 'x' ) ;
17+ const ya = trace . _yA = Axes . getFromId ( gd , trace . yaxis || 'y' , 'y' ) ;
1818
19- var xVals = xa . makeCalcdata ( trace , 'x' ) ;
20- var yVals = ya . makeCalcdata ( trace , 'y' ) ;
19+ const xVals = xa . makeCalcdata ( trace , 'x' ) ;
20+ const yVals = ya . makeCalcdata ( trace , 'y' ) ;
2121
22- var len = Math . min ( xVals . length , yVals . length ) ;
22+ const len = Math . min ( xVals . length , yVals . length ) ;
2323 trace . _length = len ;
24- var cd = new Array ( len ) ;
24+ const cd = new Array ( len ) ;
2525
2626 var normMin = Infinity ;
2727 var normMax = - Infinity ;
2828 var cMin = Infinity ;
2929 var cMax = - Infinity ;
30- var markerColor = ( trace . marker || { } ) . color ;
31- var hasMarkerColorArray = Lib . isArrayOrTypedArray ( markerColor ) ;
32-
33- var uArr = trace . u || [ ] ;
34- var vArr = trace . v || [ ] ;
35-
36- // First pass: build calcdata and compute maxNorm (needed for 'scaled' sizemode)
30+ const markerColor = trace . marker . color ;
31+ const hasMarkerColorArray = Lib . isArrayOrTypedArray ( markerColor ) ;
32+
33+ const uArr = trace . u || [ ] ;
34+ const vArr = trace . v || [ ] ;
35+
36+ const anglemode = trace . anglemode ;
37+ const sizemode = trace . sizemode ;
38+ const anchor = trace . anchor ;
39+ const isTip = anchor === 'tip' ;
40+ const isCenter = anchor === 'center' ;
41+
42+ // Keep track of:
43+ // - minimum and maximum x and y (for density calculation)
44+ // - number of valid (x, y) pairs (for density calculation)
45+ // - minimum and maximum u and v (for setting axis ranges)
46+ var xMin = Infinity ;
47+ var xMax = - Infinity ;
48+ var yMin = Infinity ;
49+ var yMax = - Infinity ;
50+ var uMin = Infinity ;
51+ var uMax = - Infinity ;
52+ var vMin = Infinity ;
53+ var vMax = - Infinity ;
54+ var nValid = 0 ;
55+
56+ // First pass: build calcdata, and keep track of the maximum and minimum vector norm in the trace,
57+ // to be used for sizemode 'scaled' (max norm only) and for magnitude-based colorscale range
3758 for ( var i = 0 ; i < len ; i ++ ) {
3859 var cdi = cd [ i ] = { i : i } ;
3960 var xValid = isNumeric ( xVals [ i ] ) ;
4061 var yValid = isNumeric ( yVals [ i ] ) ;
4162
4263 // Sanitize u/v: If either u or v is non-numeric (bad strings, Infinity,
43- // NaN, null, undefined) for a single point, set both to zero. Numeric strings are cast.
64+ // NaN, null, undefined) for a single point, set both to zero.
65+ // Cast numeric strings to numbers.
4466 // Store in calcdata so that the sanitized values can be reused.
4567 // Use underscore-prefixed keys because 'v' is already used by box/violin
4668 // (meaning "value") and setting it here has unintended side effects.
@@ -54,14 +76,19 @@ module.exports = function calc(gd, trace) {
5476 }
5577
5678 if ( xValid && yValid ) {
79+ nValid ++ ;
5780 cdi . x = xVals [ i ] ;
5881 cdi . y = yVals [ i ] ;
5982
60- // Only plottable points feed the ranges derived here: the vector norm
61- // range that drives 'scaled' arrow sizing (_maxNorm) and the
62- // magnitude-based colorscale range. A point that can't be drawn must
63- // not stretch either range. (u/v are already finite, so norm needs no
64- // isFinite guard.)
83+ if ( xVals [ i ] < xMin ) xMin = xVals [ i ] ;
84+ if ( xVals [ i ] > xMax ) xMax = xVals [ i ] ;
85+ if ( yVals [ i ] < yMin ) yMin = yVals [ i ] ;
86+ if ( yVals [ i ] > yMax ) yMax = yVals [ i ] ;
87+ if ( ui < uMin ) uMin = ui ;
88+ if ( ui > uMax ) uMax = ui ;
89+ if ( vi < vMin ) vMin = vi ;
90+ if ( vi > vMax ) vMax = vi ;
91+
6592 var norm = Math . sqrt ( ui * ui + vi * vi ) ;
6693 if ( norm > normMax ) normMax = norm ;
6794 if ( norm < normMin ) normMin = norm ;
@@ -79,105 +106,86 @@ module.exports = function calc(gd, trace) {
79106 }
80107 }
81108
82- // Store maxNorm for use by plot.js
109+ // Store maxNorm for use by plot step
83110 trace . _maxNorm = normMax ;
84111
85- // Compute arrow geometry for axis autorange.
86- //
87- // The v-component is drawn directly in data space, so each arrow's y-tip is
88- // exact and we expand the y-axis here with the tip coordinates. The
89- // u-component is stretched by scaleRatio = pxPerY / pxPerX in plot.js so
90- // that arrows keep their on-screen angle, which makes an arrow's *horizontal
91- // pixel extent* depend on the y-scale rather than the x-scale:
92- // |dx_px| = pxPerY * baseLen * |unitx|
93- // We therefore expand the x-axis with pixel padding (ppad) rather than
94- // data-space tips (whose data width would depend on the very x-range we are
95- // trying to compute). Since that ppad depends on the y-scale - which depends
96- // on the combined y-extent of every quiver trace sharing the axis - the
97- // x-axis expansion is finished in crossTraceCalc; here we stash the per-point
98- // geometry and y-bounds it needs.
99- var sizemode = trace . sizemode || 'scaled' ;
100- var sizeref = ( trace . sizeref !== undefined ) ? trace . sizeref : ( sizemode === 'raw' ? 1 : 0.5 ) ;
101- var anchor = trace . anchor || 'tail' ;
102- var isTip = anchor === 'tip' ;
103- var isCenter = anchor === 'center' ;
104-
105- var baseX = new Array ( len ) ;
106- var tipsY = new Array ( len * 2 ) ;
107- var geomLen = new Array ( len ) ;
108- var geomUx = new Array ( len ) ;
109-
110- var yMin = Infinity ;
111- var yMax = - Infinity ;
112-
113- for ( var k = 0 ; k < len ; k ++ ) {
114- var xk = xVals [ k ] ;
115- var yk = yVals [ k ] ;
116- var uk = cd [ k ] . _u ;
117- var vk = cd [ k ] . _v ;
118- var nk = Math . sqrt ( uk * uk + vk * vk ) ;
119-
120- var baseLen ;
121- if ( sizemode === 'scaled' ) {
122- baseLen = normMax ? ( nk / normMax ) * sizeref : 0 ;
112+ if ( sizemode === 'scaled' || anglemode === 'paper' ) {
113+ // Ignore sizemode 'raw' if anglemode is set to 'paper': always scale
114+
115+ // Compute point density of the entire trace: Area of bounding box
116+ // divided by number of points. This is used to scale arrows in
117+ // 'scaled' sizemode.
118+ // TODO: How to handle the case where there is just one point in a trace,
119+ // or all points have the same x or y value? This will give a boxArea of 0.
120+ // For now I'm going to just normalize to a vector of unit length (1) in that case,
121+ // but that's not a great solution
122+ const boxArea = ( xMax - xMin ) * ( yMax - yMin ) ;
123+ const pointDensity = boxArea / len ;
124+ // Now, compute the scale factor for scaled size mode
125+ // The scale factor should be such that
126+ // _maxNorm * _scaleFactor = Math.sqrt(_pointDensity)
127+ // Therefore: _scaleFactor = Math.sqrt(_pointDensity) / _maxNorm
128+ if ( pointDensity === 0 ) {
129+ trace . _scaleFactor = 1 / trace . _maxNorm
123130 } else {
124- baseLen = nk * sizeref ;
131+ trace . _scaleFactor = Math . sqrt ( pointDensity ) / trace . _maxNorm ;
125132 }
133+ // Note: If anglemode === 'paper', this scale factor must be
134+ // multiplied by Math.sqrt(xa._m * ya._m), but we can't do that quite yet
135+ // since the axis scales are not fully determined. Do it in plot step instead.
136+ } else { // sizemode === 'raw'
137+ // For raw sizemode, scale factor is always 1
138+ trace . _scaleFactor = 1 ;
139+ }
126140
127- var unitxk = nk ? ( uk / nk ) : 0 ;
128- var unityk = nk ? ( vk / nk ) : 0 ;
129- var dyk = unityk * baseLen ;
130-
131- geomLen [ k ] = baseLen ;
132- geomUx [ k ] = unitxk ;
133- baseX [ k ] = xk ;
134-
135- var y0 , y1 ;
136- if ( isTip ) {
137- y1 = yk ;
138- y0 = yk - dyk ;
139- } else if ( isCenter ) {
140- y0 = yk - dyk / 2 ;
141- y1 = yk + dyk / 2 ;
142- } else { // tail (default)
143- y0 = yk ;
144- y1 = yk + dyk ;
141+ // Multiply scale factor by sizeref
142+ trace . _scaleFactor *= trace . sizeref ;
143+
144+ // Now we need to compute the arrow geometry for axis autorange
145+ const xTipPositions = new Array ( len ) ;
146+ const yTipPositions = new Array ( len ) ;
147+ const xTailPositions = new Array ( len ) ;
148+ const yTailPositions = new Array ( len ) ;
149+ var arrowLenX , arrowLenY ;
150+ // Compute the x- and y-positions of the tip of each arrow,
151+ // assuming anglemode === 'data' (i.e. u/v are in data coordinates)
152+ for ( var i = 0 ; i < len ; i ++ ) {
153+ var cdi = cd [ i ] ;
154+ arrowLenX = cdi . _u * trace . _scaleFactor ;
155+ arrowLenY = cdi . _v * trace . _scaleFactor ;
156+ if ( isTip ) {
157+ xTipPositions [ i ] = cdi . x ;
158+ yTipPositions [ i ] = cdi . y ;
159+ xTailPositions [ i ] = cdi . x - arrowLenX ;
160+ yTailPositions [ i ] = cdi . y - arrowLenY ;
161+ } else if ( isCenter ) {
162+ xTipPositions [ i ] = cdi . x + arrowLenX / 2 ;
163+ yTipPositions [ i ] = cdi . y + arrowLenY / 2 ;
164+ xTailPositions [ i ] = cdi . x - arrowLenX / 2 ;
165+ yTailPositions [ i ] = cdi . y - arrowLenY / 2 ;
166+ } else { // tail
167+ xTipPositions [ i ] = cdi . x + arrowLenX ;
168+ yTipPositions [ i ] = cdi . y + arrowLenY ;
169+ xTailPositions [ i ] = cdi . x ;
170+ yTailPositions [ i ] = cdi . y ;
145171 }
146- tipsY [ k * 2 ] = y0 ;
147- tipsY [ k * 2 + 1 ] = y1 ;
172+ }
148173
149- if ( isNumeric ( y0 ) ) {
150- if ( y0 < yMin ) yMin = y0 ;
151- if ( y0 > yMax ) yMax = y0 ;
152- }
153- if ( isNumeric ( y1 ) ) {
154- if ( y1 < yMin ) yMin = y1 ;
155- if ( y1 > yMax ) yMax = y1 ;
156- }
174+ if ( anglemode === 'data' ) {
175+ // If anglemode is 'data', we can use the arrow tip positions directly to expand the axes ranges
176+ trace . _extremes [ xa . _id ] = Axes . findExtremes ( xa , xTipPositions . concat ( xTailPositions ) , { padded : true } ) ;
177+ trace . _extremes [ ya . _id ] = Axes . findExtremes ( ya , yTipPositions . concat ( yTailPositions ) , { padded : true } ) ;
178+ } else { // anglemode === 'paper'
179+ // TODO: For now, just do the same thing as for anglemode === 'data', but this is not correct.
180+ // We actually need more sophisticated logic here, since this will give a bad result
181+ // if the data aspect ratio is very different from the plot aspect ratio.
182+ trace . _extremes [ xa . _id ] = Axes . findExtremes ( xa , xTipPositions . concat ( xTailPositions ) , { padded : true } ) ;
183+ trace . _extremes [ ya . _id ] = Axes . findExtremes ( ya , yTipPositions . concat ( yTailPositions ) , { padded : true } ) ;
157184 }
158185
159186 xa . _minDtick = 0 ;
160187 ya . _minDtick = 0 ;
161188
162- // y-axis: arrow tips are exact in data space.
163- trace . _extremes [ ya . _id ] = Axes . findExtremes ( ya , tipsY , { padded : true } ) ;
164-
165- // x-axis: provisional bound from the base positions only; crossTraceCalc
166- // replaces this with a ppad-based expansion once the combined y-scale across
167- // all quiver traces on this axis is known.
168- trace . _extremes [ xa . _id ] = Axes . findExtremes ( xa , baseX , { padded : true } ) ;
169-
170- // Geometry needed to finish the x-axis expansion in crossTraceCalc.
171- trace . _quiver = {
172- baseX : baseX ,
173- geomLen : geomLen ,
174- geomUx : geomUx ,
175- isTip : isTip ,
176- isCenter : isCenter ,
177- yMin : yMin ,
178- yMax : yMax
179- } ;
180-
181189 // Merge text arrays into calcdata for Drawing.textPointStyle
182190 Lib . mergeArray ( trace . text , cd , 'tx' ) ;
183191 Lib . mergeArray ( trace . textposition , cd , 'tp' ) ;
0 commit comments