Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
bf6d887
Add scatterquiver trace type
degzhaus Oct 21, 2025
6400591
Ensure no-gl-jasmine tests pass
degzhaus Oct 22, 2025
e9551cd
Name plot type quiver not scatterquiver
degzhaus Nov 2, 2025
7b321ea
Model quiver api closer to 3d cone trace
degzhaus Nov 3, 2025
32ce7bb
Match arrowhead attributes for annotations
degzhaus Nov 11, 2025
4ef6182
Derive scaling from axes
degzhaus Nov 11, 2025
8fbc748
Remove angle attribute
degzhaus Nov 11, 2025
aa5cc61
Include colorscale attributes in quiver
degzhaus Nov 11, 2025
7b44252
Add support for coloring by an independent scalar array
degzhaus Nov 11, 2025
fb6bbee
Run npm run schema
degzhaus Nov 11, 2025
4e8446a
Use isArrayOrTypedArray
degzhaus Nov 19, 2025
34d1831
Add quiver jasmine and image tests
degzhaus Nov 17, 2025
e924f87
Generate baseline images for quiver tests
degzhaus Nov 17, 2025
e9f81df
Run npm run schema
degzhaus Nov 30, 2025
fc62e51
Improve code readability
degzhaus Dec 18, 2025
4f91255
Reuse scatter selectPoints for quiver instead of duplicating selectio…
degzhaus Feb 16, 2026
82e6bc1
Use hasColorscale() to conditionally enable colorscale instead of har…
degzhaus Feb 16, 2026
7135b52
Reuse scatter handleXYDefaults() for quiver x/y coercion, validation,…
degzhaus Feb 16, 2026
3cd525c
Move colorscale attrs under marker and replace custom c attribute wit…
degzhaus Feb 16, 2026
96845a8
Fix axis autorange to include arrow tips, not just base positions; re…
degzhaus Feb 16, 2026
6a37260
Add legend line icon for quiver traces by including quiver in getStyl…
degzhaus Feb 16, 2026
b19cce2
Add marker_colorbar config to quiver module so colorbars render corre…
degzhaus Feb 16, 2026
69516db
Implement visual text label rendering for quiver using shared Drawing…
degzhaus Feb 16, 2026
96bb82c
Implement per-arrow selected/unselected styling with opacity dimming …
degzhaus Feb 16, 2026
9a17d8a
Run npm run schema
degzhaus Feb 16, 2026
37f9d6f
Fix colorbar crash by ensuring traceOut.marker exists before colorsca…
degzhaus Feb 16, 2026
0c13caf
Regenerate image baselines
degzhaus Mar 9, 2026
2e59cda
Update unrelated baselines
degzhaus Mar 9, 2026
f79bca8
Nest line options under marker.line
degzhaus Mar 9, 2026
c7f073d
Move arrowheadsize under marker
degzhaus Mar 9, 2026
f327f5d
Run npm run schema
degzhaus Mar 9, 2026
b1e3852
Regenerate quiver custom colorscale
degzhaus Mar 9, 2026
c7c9b7d
Import scatter attributes for x, y coercion
degzhaus Mar 9, 2026
4d57892
Run npm run schema
degzhaus Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/index-strict.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Plotly.register([
require('../src/traces/scatterpolargl/strict'),
require('./barpolar'),
require('./scattersmith'),
require('./quiver'),

// components
require('./calendars'),
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Plotly.register([
require('./scatterpolargl'),
require('./barpolar'),
require('./scattersmith'),
require('./quiver'),

// components
require('./calendars'),
Expand Down
3 changes: 3 additions & 0 deletions lib/quiver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('../src/traces/quiver');
2 changes: 1 addition & 1 deletion src/components/legend/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ function getGradientDirection(reversescale, isRadial) {
function getStyleGuide(d) {
var trace = d[0].trace;
var contours = trace.contours;
var showLine = subTypes.hasLines(trace);
var showLine = subTypes.hasLines(trace) || (trace.visible && trace.type === 'quiver');
var showMarker = subTypes.hasMarkers(trace);

var showFill = trace.visible && trace.fill && trace.fill !== 'none';
Expand Down
211 changes: 211 additions & 0 deletions src/traces/quiver/attributes.js
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);

// 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;
147 changes: 147 additions & 0 deletions src/traces/quiver/calc.js
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];
colorscaleCalc(gd, trace, {
vals: vals,
containerStr: 'marker',
cLetter: 'c'
});
}

return cd;
};
Loading