Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
Ensure no-gl-jasmine tests pass
  • Loading branch information
degzhaus committed Nov 29, 2025
commit 5d4856f05ceeec879c63d740adb56b1bd5dfddb6
76 changes: 34 additions & 42 deletions src/traces/scatterquiver/calc.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,47 @@
'use strict';

var Lib = require('../../lib');
var Axes = require('../../plots/cartesian/axes');
var isNumeric = require('fast-isnumeric');
var BADNUM = require('../../constants/numerical').BADNUM;
var scatterCalc = require('../scatter/calc');

/**
* Main calculation function for scatterquiver trace
* Creates calcdata with arrow path data for each vector
*/
module.exports = function calc(gd, trace) {
var x = trace.x;
var y = trace.y;
var u = trace.u;
var v = trace.v;
var scale = trace.scale;
var arrowScale = trace.arrow_scale;
var angle = trace.angle;
var scaleRatio = trace.scaleratio;

// Create calcdata - one complete arrow per entry
var calcdata = [];
var len = x.length;
// 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');

// u/v are read in plot using the original trace arrays via cdi.i

var len = Math.min(xVals.length, yVals.length);
trace._length = len;
var cd = new Array(len);

for(var i = 0; i < len; i++) {
// Calculate arrow components
var dx = u[i] * scale * (scaleRatio || 1);
var dy = v[i] * scale;
var barbLen = Math.sqrt(dx * dx / (scaleRatio || 1) + dy * dy);
var arrowLen = barbLen * arrowScale;
var barbAng = Math.atan2(dy, dx / (scaleRatio || 1));

var ang1 = barbAng + angle;
var ang2 = barbAng - angle;

var endX = x[i] + dx;
var endY = y[i] + dy;

var point1X = endX - arrowLen * Math.cos(ang1) * (scaleRatio || 1);
var point1Y = endY - arrowLen * Math.sin(ang1);
var point2X = endX - arrowLen * Math.cos(ang2) * (scaleRatio || 1);
var point2Y = endY - arrowLen * Math.sin(ang2);

// Create complete arrow as one path: shaft + arrow head
var arrowPath = [
{ x: x[i], y: y[i], i: i }, // Start point
{ x: endX, y: endY, i: i }, // End of shaft
{ x: point1X, y: point1Y, i: i }, // Arrow head point 1
{ x: endX, y: endY, i: i }, // Back to end
{ x: point2X, y: point2Y, i: i } // Arrow head point 2
];

calcdata.push(arrowPath);
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;
}

// No additional props; keep minimal to avoid collisions with generic fields (e.g. `v`)
}

return calcdata;
// Ensure axes are expanded and categories registered like scatter traces do
scatterCalc.calcAxisExpansion(gd, trace, xa, ya, xVals, yVals);

return cd;
};
18 changes: 13 additions & 5 deletions src/traces/scatterquiver/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,24 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout

// Simple validation - check if we have the required arrays
if(!x || !Array.isArray(x) || x.length === 0 ||
!y || !Array.isArray(y) || y.length === 0 ||
!u || !Array.isArray(u) || u.length === 0 ||
!v || !Array.isArray(v) || v.length === 0) {
!y || !Array.isArray(y) || y.length === 0) {
traceOut.visible = false;
return;
}

// If u/v are missing, default to zeros so the trace participates in calc/category logic
var len = Math.min(x.length, y.length);
if(!Array.isArray(u) || u.length === 0) {
traceOut.u = new Array(len);
for(var i = 0; i < len; i++) traceOut.u[i] = 0;
}
if(!Array.isArray(v) || v.length === 0) {
traceOut.v = new Array(len);
for(var j = 0; j < len; j++) traceOut.v[j] = 0;
}

// Set basic properties
traceOut.type = 'scatterquiver';
traceOut.visible = true;

// Set default values using coerce
coerce('scale', 0.1);
Expand Down Expand Up @@ -64,5 +72,5 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('unselected.textfont.color');

// Set the data length
traceOut._length = x.length;
traceOut._length = len;
};
38 changes: 19 additions & 19 deletions src/traces/scatterquiver/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,47 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
var xpx = xa.c2p(xval);
var ypx = ya.c2p(yval);

// Find the closest arrow to the hover point
// Find the closest arrow base point to the hover point
var minDistance = Infinity;
var closestPoint = null;
var closestIndex = -1;

// Check each arrow segment
// Each cd[i] is a calcdata point object with x/y
for(var i = 0; i < cd.length; i++) {
var segment = cd[i];
if(segment.length < 2) continue;
var cdi = cd[i];
if(cdi.x === undefined || cdi.y === undefined) continue;

var px = xa.c2p(cdi.x);
var py = ya.c2p(cdi.y);

var distance = Math.sqrt((xpx - px) * (xpx - px) + (ypx - py) * (ypx - py));

// Calculate distance to the start point of the arrow
var x1 = xa.c2p(segment[0].x);
var y1 = ya.c2p(segment[0].y);

var distance = Math.sqrt((xpx - x1) * (xpx - x1) + (ypx - y1) * (ypx - y1));

if(distance < minDistance) {
minDistance = distance;
closestPoint = segment[0]; // Use the start point for hover data
closestPoint = cdi;
closestIndex = i;
}
}

if(!closestPoint || minDistance > (trace.hoverdistance || 20)) return;
var maxHoverDist = pointData.distance === Infinity ? Infinity : (trace.hoverdistance || 20);
if(!closestPoint || minDistance > maxHoverDist) return;

// Create hover point data with proper label values and spikeline support
var hoverPoint = {
x: closestPoint.x,
y: closestPoint.y,
u: trace.u[closestIndex],
v: trace.v[closestIndex],
text: trace.text ? trace.text[closestIndex] : '',
u: trace.u ? trace.u[closestIndex] : undefined,
v: trace.v ? trace.v[closestIndex] : undefined,
text: Array.isArray(trace.text) ? trace.text[closestIndex] : trace.text,
name: trace.name || '',
trace: trace,
index: closestIndex,
// Set label values for proper hover formatting
// Label values for formatting
xLabelVal: closestPoint.x,
yLabelVal: closestPoint.y,
uLabelVal: trace.u[closestIndex],
vLabelVal: trace.v[closestIndex],
// Add spikeline support
uLabelVal: trace.u ? trace.u[closestIndex] : undefined,
vLabelVal: trace.v ? trace.v[closestIndex] : undefined,
// Spikeline support
xa: pointData.xa,
ya: pointData.ya,
x0: closestPoint.x,
Expand Down
62 changes: 45 additions & 17 deletions src/traces/scatterquiver/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition

Drawing.setClipUrl(lines, plotinfo.layerClipId, gd);

// Create line segments for each arrow
// Create one path per data point (arrow)
var lineSegments = lines.selectAll('path.js-line')
.data(cdscatter);

Expand All @@ -82,26 +82,54 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
lineSegments.exit().remove();

// Update line segments
lineSegments.each(function(d) {
lineSegments.each(function(cdi) {
var path = d3.select(this);
var segment = d;

if(segment.length === 0) return;

// Convert data coordinates to pixel coordinates
var pixelCoords = segment.map(function(point) {
return {
x: xa.c2p(point.x),
y: ya.c2p(point.y)
};
});

// Create SVG path from pixel coordinates
var pathData = 'M' + pixelCoords[0].x + ',' + pixelCoords[0].y;
for(var i = 1; i < pixelCoords.length; i++) {
pathData += 'L' + pixelCoords[i].x + ',' + pixelCoords[i].y;
// Skip invalid points
if(cdi.x === undefined || cdi.y === undefined) {
path.attr('d', null);
return;
}

// Compute arrow in data space
var scale = trace.scale || 1;
var scaleRatio = trace.scaleratio || 1;
var arrowScale = trace.arrow_scale || 0.2;
var angle = trace.angle || Math.PI / 12; // small default

var u = (trace.u && trace.u[cdi.i]) || 0;
var v = (trace.v && trace.v[cdi.i]) || 0;

var dx = u * scale * scaleRatio;
var dy = v * scale;
var barbLen = Math.sqrt((dx * dx) / scaleRatio + dy * dy);
var arrowLen = barbLen * arrowScale;
var barbAng = Math.atan2(dy, dx / scaleRatio);

var ang1 = barbAng + angle;
var ang2 = barbAng - angle;

var x0 = cdi.x;
var y0 = cdi.y;
var x1 = x0 + dx;
var y1 = y0 + dy;

var xh1 = x1 - arrowLen * Math.cos(ang1) * scaleRatio;
var yh1 = y1 - arrowLen * Math.sin(ang1);
var xh2 = x1 - arrowLen * Math.cos(ang2) * scaleRatio;
var yh2 = y1 - arrowLen * Math.sin(ang2);

// Convert to pixels
var p0x = xa.c2p(x0);
var p0y = ya.c2p(y0);
var p1x = xa.c2p(x1);
var p1y = ya.c2p(y1);
var ph1x = xa.c2p(xh1);
var ph1y = ya.c2p(yh1);
var ph2x = xa.c2p(xh2);
var ph2y = ya.c2p(yh2);

var pathData = 'M' + p0x + ',' + p0y + 'L' + p1x + ',' + p1y + 'L' + ph1x + ',' + ph1y + 'L' + p1x + ',' + p1y + 'L' + ph2x + ',' + ph2y;
path.attr('d', pathData);
});

Expand Down