-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Add quiver trace type for vector field visualization #7710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0d30a84
4dc1b41
f668ed8
685b35f
4bbc9cd
9b39b66
e307c76
03d3ea3
dd01de3
fb775af
220c67b
836fdc8
229fac7
fa18183
59db7f7
4a081b6
a7cca35
8bd734d
a651c5d
2affd92
2fc3948
658546a
77093ea
ed8f205
4a2c62f
1ee4ab1
263d2c9
44d5c32
2d792cb
6e07967
78998fd
1196b83
d2ab99a
b8559e2
1a15aa3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| 'use strict'; | ||
|
|
||
| module.exports = require('../src/traces/quiver'); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,211 @@ | ||||||
| 'use strict'; | ||||||
|
|
||||||
| var baseAttrs = require('../../plots/attributes'); | ||||||
| var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; | ||||||
| var fontAttrs = require('../../plots/font_attributes'); | ||||||
| var axisHoverFormat = require('../../plots/cartesian/axis_format_attributes').axisHoverFormat; | ||||||
| var extendFlat = require('../../lib/extend').extendFlat; | ||||||
| var colorScaleAttrs = require('../../components/colorscale/attributes'); | ||||||
| var dash = require('../../components/drawing/attributes').dash; | ||||||
| var annotationAttrs = require('../../components/annotations/attributes'); | ||||||
| var scatterAttrs = require('../scatter/attributes'); | ||||||
|
|
||||||
| var attrs = { | ||||||
| x: { | ||||||
| valType: 'data_array', | ||||||
| editType: 'calc+clearAxisTypes', | ||||||
| anim: true, | ||||||
| description: 'Sets the x coordinates of the arrow locations.' | ||||||
| }, | ||||||
| x0: scatterAttrs.x0, | ||||||
| dx: scatterAttrs.dx, | ||||||
| y: { | ||||||
| valType: 'data_array', | ||||||
| editType: 'calc+clearAxisTypes', | ||||||
| anim: true, | ||||||
| description: 'Sets the y coordinates of the arrow locations.' | ||||||
| }, | ||||||
| y0: scatterAttrs.y0, | ||||||
| dy: scatterAttrs.dy, | ||||||
| u: { | ||||||
| valType: 'data_array', | ||||||
| editType: 'calc', | ||||||
| anim: true, | ||||||
| description: 'Sets the x components of the arrow vectors.' | ||||||
| }, | ||||||
| v: { | ||||||
| valType: 'data_array', | ||||||
| editType: 'calc', | ||||||
| anim: true, | ||||||
| description: 'Sets the y components of the arrow vectors.' | ||||||
| }, | ||||||
| sizemode: { | ||||||
| valType: 'enumerated', | ||||||
| values: ['scaled', 'absolute', 'raw'], | ||||||
| editType: 'calc', | ||||||
| dflt: 'scaled', | ||||||
| description: [ | ||||||
| 'Determines whether `sizeref` is set as a *scaled* (unitless) scalar', | ||||||
| '(normalized by the max u/v norm in the vector field), as an *absolute*', | ||||||
| 'value (in the same units as the vector field), or *raw* to use the', | ||||||
| 'raw vector lengths.' | ||||||
| ].join(' ') | ||||||
| }, | ||||||
| sizeref: { | ||||||
| valType: 'number', | ||||||
| min: 0, | ||||||
| editType: 'calc', | ||||||
| description: [ | ||||||
| 'Adjusts the arrow size scaling.', | ||||||
| 'The arrow length is determined by the vector norm multiplied by `sizeref`,', | ||||||
| 'optionally normalized when `sizemode` is *scaled*.' | ||||||
| ].join(' ') | ||||||
| }, | ||||||
| anchor: { | ||||||
| valType: 'enumerated', | ||||||
| values: ['tip', 'tail', 'cm', 'center', 'middle'], | ||||||
| dflt: 'tail', | ||||||
| editType: 'calc', | ||||||
| description: [ | ||||||
| 'Sets the arrows\' anchor with respect to their (x,y) positions.', | ||||||
| 'Use *tail* to place (x,y) at the base, *tip* to place (x,y) at the head,', | ||||||
| 'or *cm*/*center*/*middle* to center the arrow on (x,y).' | ||||||
| ].join(' ') | ||||||
| }, | ||||||
| hoverdistance: { | ||||||
| valType: 'number', | ||||||
| min: -1, | ||||||
| dflt: 20, | ||||||
| editType: 'calc', | ||||||
| description: 'Maximum distance (in pixels) to look for nearby arrows on hover.' | ||||||
| }, | ||||||
|
|
||||||
| xhoverformat: axisHoverFormat('x'), | ||||||
| yhoverformat: axisHoverFormat('y'), | ||||||
| uhoverformat: axisHoverFormat('u', 'noDate'), | ||||||
| vhoverformat: axisHoverFormat('v', 'noDate'), | ||||||
|
|
||||||
| // Text and labels | ||||||
| text: { | ||||||
| valType: 'data_array', | ||||||
| editType: 'calc', | ||||||
| anim: true, | ||||||
| description: 'Sets text elements associated with each (x,y) pair.' | ||||||
| }, | ||||||
| textposition: { | ||||||
| valType: 'enumerated', | ||||||
| values: [ | ||||||
| 'top left', 'top center', 'top right', | ||||||
| 'middle left', 'middle center', 'middle right', | ||||||
| 'bottom left', 'bottom center', 'bottom right' | ||||||
| ], | ||||||
| dflt: 'middle center', | ||||||
| editType: 'calc', | ||||||
| description: 'Sets the positions of the `text` elements with respects to the (x,y) coordinates.' | ||||||
| }, | ||||||
| // Text font | ||||||
| textfont: fontAttrs({ | ||||||
| editType: 'calc', | ||||||
| colorEditType: 'style', | ||||||
| arrayOk: true, | ||||||
| description: 'Sets the text font.' | ||||||
| }), | ||||||
|
|
||||||
| // Marker: color, colorscale, arrowhead sizing, and line styling for arrows | ||||||
| marker: extendFlat( | ||||||
| { | ||||||
| arrowsize: extendFlat({}, annotationAttrs.arrowsize, { | ||||||
| editType: 'calc', | ||||||
| description: [ | ||||||
| 'Sets the size of the arrow head relative to `marker.line.width`.', | ||||||
| 'A value of 1 (default) gives a head about 3x as wide as the line.' | ||||||
| ].join(' ') | ||||||
| }), | ||||||
| line: { | ||||||
| width: { | ||||||
| valType: 'number', | ||||||
| min: 0, | ||||||
| dflt: 1, | ||||||
| editType: 'style', | ||||||
| description: 'Sets the width (in px) of the arrow lines.' | ||||||
| }, | ||||||
| dash: dash, | ||||||
| editType: 'style' | ||||||
| }, | ||||||
| editType: 'calc' | ||||||
| }, | ||||||
| colorScaleAttrs('marker', { | ||||||
| showScaleDflt: true, | ||||||
| editTypeOverride: 'calc' | ||||||
| }) | ||||||
| ), | ||||||
|
|
||||||
| // Selection and styling | ||||||
| selected: { | ||||||
| line: { | ||||||
| color: { | ||||||
| valType: 'color', | ||||||
| editType: 'style', | ||||||
| description: 'Sets the line color of selected points.' | ||||||
| }, | ||||||
| width: { | ||||||
| valType: 'number', | ||||||
| min: 0, | ||||||
| editType: 'style', | ||||||
| description: 'Sets the line width of selected points.' | ||||||
| }, | ||||||
| editType: 'style' | ||||||
| }, | ||||||
| textfont: { | ||||||
| color: { | ||||||
| valType: 'color', | ||||||
| editType: 'style', | ||||||
| description: 'Sets the text font color of selected points, applied only when a selection exists.' | ||||||
| }, | ||||||
| editType: 'style' | ||||||
| }, | ||||||
| editType: 'style' | ||||||
| }, | ||||||
| unselected: { | ||||||
| line: { | ||||||
| color: { | ||||||
| valType: 'color', | ||||||
| editType: 'style', | ||||||
| description: 'Sets the line color of unselected points.' | ||||||
| }, | ||||||
| width: { | ||||||
| valType: 'number', | ||||||
| min: 0, | ||||||
| editType: 'style', | ||||||
| description: 'Sets the line width of unselected points.' | ||||||
| }, | ||||||
| editType: 'style' | ||||||
| }, | ||||||
| textfont: { | ||||||
| color: { | ||||||
| valType: 'color', | ||||||
| editType: 'style', | ||||||
| description: 'Sets the text font color of unselected points, applied only when a selection exists.' | ||||||
| }, | ||||||
| editType: 'style' | ||||||
| }, | ||||||
| editType: 'style' | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| // Extend with base attributes (includes hoverinfo, etc.) | ||||||
| extendFlat(attrs, baseAttrs); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you mean to pasate
Suggested change
|
||||||
|
|
||||||
| // Add hoverinfo with proper flags for quiver | ||||||
| // We need to create a new object to avoid mutating the shared base attributes | ||||||
| attrs.hoverinfo = extendFlat({}, baseAttrs.hoverinfo, { | ||||||
| flags: ['x', 'y', 'u', 'v', 'text', 'name'], | ||||||
| dflt: 'all' | ||||||
| }); | ||||||
|
|
||||||
| // Add hovertemplate | ||||||
| attrs.hovertemplate = extendFlat({}, hovertemplateAttrs({}, { | ||||||
| keys: ['x', 'y', 'u', 'v', 'text', 'name'] | ||||||
| })); | ||||||
|
|
||||||
| module.exports = attrs; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| 'use strict'; | ||
|
|
||
| var Lib = require('../../lib'); | ||
| var Axes = require('../../plots/cartesian/axes'); | ||
| var isNumeric = require('fast-isnumeric'); | ||
| var BADNUM = require('../../constants/numerical').BADNUM; | ||
| var colorscaleCalc = require('../../components/colorscale/calc'); | ||
|
|
||
| /** | ||
| * Main calculation function for quiver trace | ||
| * Creates calcdata with arrow path data for each vector | ||
| */ | ||
| module.exports = function calc(gd, trace) { | ||
| // Map x/y through axes so category/date values become numeric calcdata | ||
| var xa = trace._xA = Axes.getFromId(gd, trace.xaxis || 'x', 'x'); | ||
| var ya = trace._yA = Axes.getFromId(gd, trace.yaxis || 'y', 'y'); | ||
|
|
||
| var xVals = xa.makeCalcdata(trace, 'x'); | ||
| var yVals = ya.makeCalcdata(trace, 'y'); | ||
|
|
||
| var len = Math.min(xVals.length, yVals.length); | ||
| trace._length = len; | ||
| var cd = new Array(len); | ||
|
|
||
| var normMin = Infinity; | ||
| var normMax = -Infinity; | ||
| var cMin = Infinity; | ||
| var cMax = -Infinity; | ||
| var markerColor = (trace.marker || {}).color; | ||
| var hasMarkerColorArray = Lib.isArrayOrTypedArray(markerColor); | ||
|
|
||
| var uArr = trace.u || []; | ||
| var vArr = trace.v || []; | ||
|
|
||
| // First pass: build calcdata and compute maxNorm (needed for 'scaled' sizemode) | ||
| for(var i = 0; i < len; i++) { | ||
| var cdi = cd[i] = { i: i }; | ||
| var xValid = isNumeric(xVals[i]); | ||
| var yValid = isNumeric(yVals[i]); | ||
|
|
||
| if(xValid && yValid) { | ||
| cdi.x = xVals[i]; | ||
| cdi.y = yVals[i]; | ||
| } else { | ||
| cdi.x = BADNUM; | ||
| cdi.y = BADNUM; | ||
| } | ||
|
|
||
| var ui = uArr[i] || 0; | ||
| var vi = vArr[i] || 0; | ||
| var norm = Math.sqrt(ui * ui + vi * vi); | ||
|
|
||
| if(isFinite(norm)) { | ||
| if(norm > normMax) normMax = norm; | ||
| if(norm < normMin) normMin = norm; | ||
| } | ||
|
|
||
| if(hasMarkerColorArray) { | ||
| var ci = markerColor[i]; | ||
| if(isNumeric(ci)) { | ||
| if(ci < cMin) cMin = ci; | ||
| if(ci > cMax) cMax = ci; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Store maxNorm for use by plot.js | ||
| trace._maxNorm = normMax; | ||
|
|
||
| // Compute arrow endpoints for axis expansion. | ||
| // We approximate with scaleRatio=1 (exact for square plots, | ||
| // close enough for autorange padding in non-square plots). | ||
| var sizemode = trace.sizemode || 'scaled'; | ||
| var sizeref = (trace.sizeref !== undefined) ? trace.sizeref : (sizemode === 'raw' ? 1 : 0.5); | ||
| var anchor = trace.anchor || 'tail'; | ||
|
|
||
| var allX = new Array(len * 2); | ||
| var allY = new Array(len * 2); | ||
|
|
||
| for(var k = 0; k < len; k++) { | ||
| var xk = xVals[k]; | ||
| var yk = yVals[k]; | ||
| var uk = uArr[k] || 0; | ||
| var vk = vArr[k] || 0; | ||
| var nk = Math.sqrt(uk * uk + vk * vk); | ||
|
|
||
| var baseLen; | ||
| if(sizemode === 'scaled') { | ||
| baseLen = normMax ? (nk / normMax) * sizeref : 0; | ||
| } else { | ||
| baseLen = nk * sizeref; | ||
| } | ||
|
|
||
| var unitxk = nk ? (uk / nk) : 0; | ||
| var unityk = nk ? (vk / nk) : 0; | ||
| var dxk = unitxk * baseLen; | ||
| var dyk = unityk * baseLen; | ||
|
|
||
| if(anchor === 'tip') { | ||
| allX[k * 2] = xk; | ||
| allY[k * 2] = yk; | ||
| allX[k * 2 + 1] = xk - dxk; | ||
| allY[k * 2 + 1] = yk - dyk; | ||
| } else if(anchor === 'cm' || anchor === 'center' || anchor === 'middle') { | ||
| allX[k * 2] = xk - dxk / 2; | ||
| allY[k * 2] = yk - dyk / 2; | ||
| allX[k * 2 + 1] = xk + dxk / 2; | ||
| allY[k * 2 + 1] = yk + dyk / 2; | ||
| } else { // tail (default) | ||
| allX[k * 2] = xk; | ||
| allY[k * 2] = yk; | ||
| allX[k * 2 + 1] = xk + dxk; | ||
| allY[k * 2 + 1] = yk + dyk; | ||
| } | ||
| } | ||
|
|
||
| // Expand axes to include both base positions and arrow tips | ||
| xa._minDtick = 0; | ||
| ya._minDtick = 0; | ||
|
|
||
| trace._extremes[xa._id] = Axes.findExtremes(xa, allX, {padded: true}); | ||
| trace._extremes[ya._id] = Axes.findExtremes(ya, allY, {padded: true}); | ||
|
|
||
| // Merge text arrays into calcdata for Drawing.textPointStyle | ||
| Lib.mergeArray(trace.text, cd, 'tx'); | ||
| Lib.mergeArray(trace.textposition, cd, 'tp'); | ||
| if(trace.textfont) { | ||
| Lib.mergeArrayCastPositive(trace.textfont.size, cd, 'ts'); | ||
| Lib.mergeArray(trace.textfont.color, cd, 'tc'); | ||
| Lib.mergeArray(trace.textfont.family, cd, 'tf'); | ||
| Lib.mergeArray(trace.textfont.weight, cd, 'tw'); | ||
| Lib.mergeArray(trace.textfont.style, cd, 'ty'); | ||
| Lib.mergeArray(trace.textfont.variant, cd, 'tv'); | ||
| } | ||
|
|
||
| // Colorscale cmin/cmax computation: prefer provided marker.color, else magnitude | ||
| if(trace._hasColorscale) { | ||
| var vals = hasMarkerColorArray ? [cMin, cMax] : [normMin, normMax]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a guard (here or elsewhere) that handles if |
||
| colorscaleCalc(gd, trace, { | ||
| vals: vals, | ||
| containerStr: 'marker', | ||
| cLetter: 'c' | ||
| }); | ||
| } | ||
|
|
||
| return cd; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please remove
'cm'and'center'as values? One term for the middle should be fine.