Skip to content

Commit e51e23e

Browse files
committed
ensure arrowhead size remains constant in pixel coordinates, and cap arrowhead length at 0.7 of arrow body length
1 parent 131c00c commit e51e23e

1 file changed

Lines changed: 52 additions & 46 deletions

File tree

src/traces/quiver/plot.js

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ var Lib = require('../../lib');
66
var Drawing = require('../../components/drawing');
77
var colorscaleStroke = require('./style').colorscaleStroke;
88

9+
// Length (px) of each arrowhead arm per unit of marker.line.width at
10+
// arrowsize = 1. With the head's half-angle of PI/12, this yields an opening
11+
// roughly 3x the line width, matching the `marker.arrowsize` spec
12+
var HEAD_LEN_PER_WIDTH = 5.8;
13+
14+
// Max arrowhead length as a fraction of the body length, so the head stays
15+
// slightly shorter than the body for very short arrows
16+
var MAX_HEAD_FRAC = 0.7;
17+
918
module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transitionOpts, makeOnCompleteCallback) {
1019
var join, onComplete;
1120

@@ -90,7 +99,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
9099

91100
// Update line segments
92101
lineSegments.each(function(cdi) {
93-
var path = d3.select(this);
102+
const path = d3.select(this);
94103

95104
// Skip invalid points
96105
if(cdi.x === undefined || cdi.y === undefined) {
@@ -100,25 +109,22 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
100109

101110
// Compute arrow in data space
102111
// Derive pixel-per-data scaling from axes at this point
103-
var pxPerX = Math.abs(xa.c2p(cdi.x + 1) - xa.c2p(cdi.x));
104-
var pxPerY = Math.abs(ya.c2p(cdi.y + 1) - ya.c2p(cdi.y));
105-
var scaleRatio = (pxPerX && pxPerY) ? (pxPerY / pxPerX) : 1;
106-
var baseHeadScale = 0.2;
107-
var markerArrowsize = (trace.marker || {}).arrowsize;
108-
var arrowScale = (markerArrowsize !== undefined)
109-
? (baseHeadScale * markerArrowsize)
110-
: baseHeadScale;
111-
// Fixed arrowhead wedge angle (radians).
112-
// Arrow direction is fully determined by u,v (see barbAng below);
113-
// this constant only controls the opening of the head.
114-
var headAngle = Math.PI / 12;
115-
116-
var u = (trace.u && trace.u[cdi.i]) || 0;
117-
var v = (trace.v && trace.v[cdi.i]) || 0;
118-
119-
var norm = Math.sqrt(u * u + v * v);
120-
var unitx = norm ? (u / norm) : 0;
121-
var unity = norm ? (v / norm) : 0;
112+
const pxPerX = Math.abs(xa.c2p(cdi.x + 1) - xa.c2p(cdi.x));
113+
const pxPerY = Math.abs(ya.c2p(cdi.y + 1) - ya.c2p(cdi.y));
114+
const scaleRatio = (pxPerX && pxPerY) ? (pxPerY / pxPerX) : 1;
115+
const markerArrowsize = (trace.marker || {}).arrowsize;
116+
const arrowSizeVal = (markerArrowsize !== undefined) ? markerArrowsize : 1;
117+
// Fixed arrowhead wedge angle (radians). Arrow direction is fully
118+
// determined by u,v (see angPx below); this constant only controls the
119+
// relative angle of the point of the arrowhead
120+
const headAngle = Math.PI / 12;
121+
122+
const u = (trace.u && trace.u[cdi.i]) || 0;
123+
const v = (trace.v && trace.v[cdi.i]) || 0;
124+
125+
const norm = Math.sqrt(u * u + v * v);
126+
const unitx = norm ? (u / norm) : 0;
127+
const unity = norm ? (v / norm) : 0;
122128
var baseLen;
123129
if(sizemode === 'scaled') {
124130
var n = maxNorm ? (norm / maxNorm) : 0;
@@ -127,16 +133,10 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
127133
baseLen = norm * sizeref;
128134
}
129135

130-
var dxBase = unitx * baseLen;
131-
var dyBase = unity * baseLen;
132-
var dx = dxBase * scaleRatio;
133-
var dy = dyBase;
134-
var barbLen = Math.sqrt((dx * dx) / scaleRatio + dy * dy);
135-
var arrowLen = barbLen * arrowScale;
136-
var barbAng = Math.atan2(dy, dx / scaleRatio);
137-
138-
var ang1 = barbAng + headAngle;
139-
var ang2 = barbAng - headAngle;
136+
const dxBase = unitx * baseLen;
137+
const dyBase = unity * baseLen;
138+
const dx = dxBase * scaleRatio;
139+
const dy = dyBase;
140140

141141
var x0, y0, x1, y1;
142142
if(anchor === 'tip') {
@@ -156,22 +156,28 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
156156
y1 = y0 + dy;
157157
}
158158

159-
var xh1 = x1 - arrowLen * Math.cos(ang1) * scaleRatio;
160-
var yh1 = y1 - arrowLen * Math.sin(ang1);
161-
var xh2 = x1 - arrowLen * Math.cos(ang2) * scaleRatio;
162-
var yh2 = y1 - arrowLen * Math.sin(ang2);
163-
164-
// Convert to pixels
165-
var p0x = xa.c2p(x0);
166-
var p0y = ya.c2p(y0);
167-
var p1x = xa.c2p(x1);
168-
var p1y = ya.c2p(y1);
169-
var ph1x = xa.c2p(xh1);
170-
var ph1y = ya.c2p(yh1);
171-
var ph2x = xa.c2p(xh2);
172-
var ph2y = ya.c2p(yh2);
173-
174-
var pathData = 'M' + p0x + ',' + p0y + 'L' + p1x + ',' + p1y + 'L' + ph1x + ',' + ph1y + 'L' + p1x + ',' + p1y + 'L' + ph2x + ',' + ph2y;
159+
// Arrow body endpoints (px)
160+
const p0x = xa.c2p(x0);
161+
const p0y = ya.c2p(y0);
162+
const p1x = xa.c2p(x1);
163+
const p1y = ya.c2p(y1);
164+
165+
// Arrowhead is sized in pixels (relative to the line width)
166+
// so it remains the same regardless of zoom, rather than scaling with the data space
167+
// Set max head size so the head stays slightly shorter than the arrow body
168+
// (e.g. for very short arrows when zoomed out). No head for zero-length arrows.
169+
const lineWidth = trace.marker.line.width;
170+
const bodyLenPx = Math.sqrt((p1x - p0x) * (p1x - p0x) + (p1y - p0y) * (p1y - p0y));
171+
const maxHeadPx = MAX_HEAD_FRAC * bodyLenPx;
172+
const headLenPx = Math.min(HEAD_LEN_PER_WIDTH * arrowSizeVal * lineWidth, maxHeadPx);
173+
const angPx = Math.atan2(p1y - p0y, p1x - p0x);
174+
175+
const ph1x = p1x - headLenPx * Math.cos(angPx - headAngle);
176+
const ph1y = p1y - headLenPx * Math.sin(angPx - headAngle);
177+
const ph2x = p1x - headLenPx * Math.cos(angPx + headAngle);
178+
const ph2y = p1y - headLenPx * Math.sin(angPx + headAngle);
179+
180+
const pathData = 'M' + p0x + ',' + p0y + 'L' + p1x + ',' + p1y + 'L' + ph1x + ',' + ph1y + 'L' + p1x + ',' + p1y + 'L' + ph2x + ',' + ph2y;
175181
path.attr('d', pathData);
176182
});
177183

0 commit comments

Comments
 (0)