Skip to content

Commit 574aa52

Browse files
committed
add auto backoff
1 parent 5f8a8f1 commit 574aa52

20 files changed

+810
-69
lines changed

src/components/drawing/index.js

+106-21
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ var SYMBOLDEFS = require('./symbol_defs');
221221

222222
drawing.symbolNames = [];
223223
drawing.symbolFuncs = [];
224+
drawing.symbolBackOffs = [];
224225
drawing.symbolNeedLines = {};
225226
drawing.symbolNoDot = {};
226227
drawing.symbolNoFill = {};
@@ -240,6 +241,7 @@ Object.keys(SYMBOLDEFS).forEach(function(k) {
240241
);
241242
drawing.symbolNames[n] = k;
242243
drawing.symbolFuncs[n] = symDef.f;
244+
drawing.symbolBackOffs[n] = symDef.backoff || 0;
243245

244246
if(symDef.needLine) {
245247
drawing.symbolNeedLines[n] = true;
@@ -1072,6 +1074,26 @@ drawing.smoothclosed = function(pts, smoothness) {
10721074
return path;
10731075
};
10741076

1077+
var lastDrawnX, lastDrawnY;
1078+
1079+
function roundEnd(pt, isY, isLastPoint) {
1080+
if(isLastPoint) pt = applyBackoff(pt);
1081+
1082+
return isY ? roundY(pt[1]) : roundX(pt[0]);
1083+
}
1084+
1085+
function roundX(p) {
1086+
var v = d3.round(p, 2);
1087+
lastDrawnX = v;
1088+
return v;
1089+
}
1090+
1091+
function roundY(p) {
1092+
var v = d3.round(p, 2);
1093+
lastDrawnY = v;
1094+
return v;
1095+
}
1096+
10751097
function makeTangent(prevpt, thispt, nextpt, smoothness) {
10761098
var d1x = prevpt[0] - thispt[0];
10771099
var d1y = prevpt[1] - thispt[1];
@@ -1085,47 +1107,106 @@ function makeTangent(prevpt, thispt, nextpt, smoothness) {
10851107
var denom2 = 3 * d1a * (d1a + d2a);
10861108
return [
10871109
[
1088-
d3.round(thispt[0] + (denom1 && numx / denom1), 2),
1089-
d3.round(thispt[1] + (denom1 && numy / denom1), 2)
1110+
roundX(thispt[0] + (denom1 && numx / denom1)),
1111+
roundY(thispt[1] + (denom1 && numy / denom1))
10901112
], [
1091-
d3.round(thispt[0] - (denom2 && numx / denom2), 2),
1092-
d3.round(thispt[1] - (denom2 && numy / denom2), 2)
1113+
roundX(thispt[0] - (denom2 && numx / denom2)),
1114+
roundY(thispt[1] - (denom2 && numy / denom2))
10931115
]
10941116
];
10951117
}
10961118

10971119
// step paths - returns a generator function for paths
10981120
// with the given step shape
10991121
var STEPPATH = {
1100-
hv: function(p0, p1) {
1101-
return 'H' + d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2);
1122+
hv: function(p0, p1, isLastPoint) {
1123+
return 'H' +
1124+
roundX(p1[0]) + 'V' +
1125+
roundEnd(p1, 1, isLastPoint);
11021126
},
1103-
vh: function(p0, p1) {
1104-
return 'V' + d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2);
1127+
vh: function(p0, p1, isLastPoint) {
1128+
return 'V' +
1129+
roundY(p1[1]) + 'H' +
1130+
roundEnd(p1, 0, isLastPoint);
11051131
},
1106-
hvh: function(p0, p1) {
1107-
return 'H' + d3.round((p0[0] + p1[0]) / 2, 2) + 'V' +
1108-
d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2);
1132+
hvh: function(p0, p1, isLastPoint) {
1133+
return 'H' +
1134+
roundX((p0[0] + p1[0]) / 2) + 'V' +
1135+
roundY(p1[1]) + 'H' +
1136+
roundEnd(p1, 0, isLastPoint);
11091137
},
1110-
vhv: function(p0, p1) {
1111-
return 'V' + d3.round((p0[1] + p1[1]) / 2, 2) + 'H' +
1112-
d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2);
1138+
vhv: function(p0, p1, isLastPoint) {
1139+
return 'V' +
1140+
roundY((p0[1] + p1[1]) / 2) + 'H' +
1141+
roundX(p1[0]) + 'V' +
1142+
roundEnd(p1, 1, isLastPoint);
11131143
}
11141144
};
1115-
var STEPLINEAR = function(p0, p1) {
1116-
return 'L' + d3.round(p1[0], 2) + ',' + d3.round(p1[1], 2);
1145+
var STEPLINEAR = function(p0, p1, isLastPoint) {
1146+
return 'L' +
1147+
roundEnd(p1, 0, isLastPoint) + ',' +
1148+
roundEnd(p1, 1, isLastPoint);
11171149
};
11181150
drawing.steps = function(shape) {
11191151
var onestep = STEPPATH[shape] || STEPLINEAR;
11201152
return function(pts) {
1121-
var path = 'M' + d3.round(pts[0][0], 2) + ',' + d3.round(pts[0][1], 2);
1122-
for(var i = 1; i < pts.length; i++) {
1123-
path += onestep(pts[i - 1], pts[i]);
1153+
var path = 'M' + roundX(pts[0][0]) + ',' + roundY(pts[0][1]);
1154+
var len = pts.length;
1155+
for(var i = 1; i < len; i++) {
1156+
path += onestep(pts[i - 1], pts[i], i === len - 1);
11241157
}
11251158
return path;
11261159
};
11271160
};
11281161

1162+
function applyBackoff(pt, start) {
1163+
var backoff = pt.backoff;
1164+
var trace = pt.trace;
1165+
var d = pt.d;
1166+
var i = pt.i;
1167+
1168+
if(backoff && trace && trace.marker && trace.marker.angle === 0) {
1169+
var arrayBackoff = Lib.isArrayOrTypedArray(backoff);
1170+
var end = pt;
1171+
1172+
var x1 = start ? start[0] : lastDrawnX || 0;
1173+
var y1 = start ? start[1] : lastDrawnY || 0;
1174+
1175+
var x2 = end[0];
1176+
var y2 = end[1];
1177+
1178+
var dx = x2 - x1;
1179+
var dy = y2 - y1;
1180+
1181+
var t = Math.atan2(dy, dx);
1182+
1183+
var b = arrayBackoff ? backoff[i] : backoff;
1184+
1185+
if(b === 'auto') {
1186+
var endI = end.i;
1187+
if(trace.type === 'scatter') endI--; // Why we need this hack?
1188+
1189+
var endMarker = end.marker;
1190+
b = endMarker ? drawing.symbolBackOffs[drawing.symbolNumber(endMarker.symbol)] * endMarker.size : 0;
1191+
b += drawing.getMarkerStandoff(d[endI], trace) || 0;
1192+
}
1193+
1194+
var x = x2 - b * Math.cos(t);
1195+
var y = y2 - b * Math.sin(t);
1196+
1197+
if(
1198+
((x <= x2 && x >= x1) || (x >= x2 && x <= x1)) &&
1199+
((y <= y2 && y >= y1) || (y >= y2 && y <= y1))
1200+
) {
1201+
pt = [x, y];
1202+
}
1203+
}
1204+
1205+
return pt;
1206+
}
1207+
1208+
drawing.applyBackoff = applyBackoff;
1209+
11291210
// off-screen svg render testing element, shared by the whole page
11301211
// uses the id 'js-plotly-tester' and stores it in drawing.tester
11311212
drawing.makeTester = function() {
@@ -1452,10 +1533,12 @@ drawing.setTextPointsScale = function(selection, xScale, yScale) {
14521533
};
14531534

14541535
function getMarkerStandoff(d, trace) {
1455-
var standoff = d.mf;
1536+
var standoff;
1537+
1538+
if(d) standoff = d.mf;
14561539

14571540
if(standoff === undefined) {
1458-
standoff = trace.marker.standoff || 0;
1541+
standoff = trace.marker ? trace.marker.standoff || 0 : 0;
14591542
}
14601543

14611544
if(!trace._geo && !trace._xA) {
@@ -1467,6 +1550,8 @@ function getMarkerStandoff(d, trace) {
14671550
return standoff;
14681551
}
14691552

1553+
drawing.getMarkerStandoff = getMarkerStandoff;
1554+
14701555
var atan2 = Math.atan2;
14711556
var cos = Math.cos;
14721557
var sin = Math.sin;

src/components/drawing/symbol_defs.js

+2
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,7 @@ module.exports = {
682682
'Z'
683683
);
684684
},
685+
backoff: 0.95,
685686
noDot: true
686687
},
687688
'arrow-wide': {
@@ -700,6 +701,7 @@ module.exports = {
700701
'Z'
701702
);
702703
},
704+
backoff: 0.4,
703705
noDot: true
704706
}
705707
};

src/traces/scatter/attributes.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -295,12 +295,13 @@ module.exports = {
295295
backoff: { // TODO: do we want to have a similar option for the start of the line? If so what the name should be?
296296
valType: 'number',
297297
min: 0,
298-
dflt: 0,
298+
dflt: 'auto',
299299
arrayOk: true,
300300
editType: 'plot',
301301
description: [
302302
'Sets the line back off from the end point of the nth line segment (in px).',
303303
'This option is useful e.g. to avoid overlap with arrowhead markers.',
304+
'With *auto* the lines would trim before markers if `marker.angleref` is set to *previous*.'
304305
].join(' ')
305306
},
306307
simplify: {

src/traces/scatter/line_points.js

+31-34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
var Drawing = require('../../components/drawing');
34
var numConstants = require('../../constants/numerical');
45
var BADNUM = numConstants.BADNUM;
56
var LOG_CLIP = numConstants.LOG_CLIP;
@@ -12,18 +13,20 @@ var constants = require('./constants');
1213

1314

1415
module.exports = function linePoints(d, opts) {
16+
var trace = opts.trace || {};
1517
var xa = opts.xaxis;
1618
var ya = opts.yaxis;
1719
var xLog = xa.type === 'log';
1820
var yLog = ya.type === 'log';
1921
var xLen = xa._length;
2022
var yLen = ya._length;
2123
var backoff = opts.backoff;
24+
var marker = trace.marker;
2225
var connectGaps = opts.connectGaps;
2326
var baseTolerance = opts.baseTolerance;
2427
var shape = opts.shape;
2528
var linear = shape === 'linear';
26-
var fill = opts.fill && opts.fill !== 'none';
29+
var fill = trace.fill && trace.fill !== 'none';
2730
var segments = [];
2831
var minTolerance = constants.minTolerance;
2932
var len = d.length;
@@ -276,7 +279,17 @@ module.exports = function linePoints(d, opts) {
276279
lastXEdge = lastYEdge = 0;
277280
}
278281

282+
var arrayMarker = Lib.isArrayOrTypedArray(marker);
283+
279284
function addPt(pt) {
285+
if(pt && backoff) {
286+
pt.i = i;
287+
pt.d = d;
288+
pt.trace = trace;
289+
pt.marker = arrayMarker ? marker[pt.i] : marker;
290+
pt.backoff = backoff;
291+
}
292+
280293
latestXFrac = pt[0] / xLen;
281294
latestYFrac = pt[1] / yLen;
282295
// Are we more than maxScreensAway off-screen any direction?
@@ -447,51 +460,35 @@ module.exports = function linePoints(d, opts) {
447460
segments.push(pts.slice(0, pti));
448461
}
449462

450-
if(backoff) {
463+
464+
var lastShapeChar = shape.slice(shape.length - 1);
465+
if(backoff && lastShapeChar !== 'h' && lastShapeChar !== 'v') {
466+
var trimmed = false;
467+
var n = -1;
451468
var newSegments = [];
452-
var arrayBackoff = Lib.isArrayOrTypedArray(backoff);
453-
var lastShapeChar = shape.slice(shape.length - 1);
454-
i = -1;
469+
455470
for(var j = 0; j < segments.length; j++) {
456-
i++;
457471
for(var k = 0; k < segments[j].length - 1; k++) {
458472
var start = segments[j][k];
459473
var end = segments[j][k + 1];
460474

461-
var x1 = start[0];
462-
var y1 = start[1];
463-
464-
var x2 = end[0];
465-
var y2 = end[1];
466-
467-
var dx = x2 - x1;
468-
var dy = y2 - y1;
469-
470-
var t = Math.atan2(dy, dx);
471-
if(lastShapeChar === 'h') {
472-
t = dx < 0 ? Math.PI : 0;
473-
} else if(lastShapeChar === 'v') {
474-
t = dy > 0 ? Math.PI / 2 : -Math.PI / 2;
475-
}
476-
477-
var b = arrayBackoff ? backoff[i] : backoff;
478-
479-
var x = x2 - b * Math.cos(t);
480-
var y = y2 - b * Math.sin(t);
481-
475+
var xy = Drawing.applyBackoff(end, start);
482476
if(
483-
((x <= x2 && x >= x1) || (x >= x2 && x <= x1)) &&
484-
((y <= y2 && y >= y1) || (y >= y2 && y <= y1))
477+
xy[0] !== end[0] ||
478+
xy[1] !== end[1]
485479
) {
486-
if(!newSegments[j]) newSegments[j] = [];
487-
488-
newSegments[j].push(start);
489-
newSegments[j].push([x, y]);
480+
trimmed = true;
481+
}
482+
if(!newSegments[n + 1]) {
483+
n++;
484+
newSegments[n] = [
485+
start, [xy[0], xy[1]]
486+
];
490487
}
491488
}
492489
}
493490

494-
return newSegments;
491+
return trimmed ? newSegments : segments;
495492
}
496493

497494
return segments;

src/traces/scatter/plot.js

+1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
209209
segments = linePoints(cdscatter, {
210210
xaxis: xa,
211211
yaxis: ya,
212+
trace: trace,
212213
connectGaps: trace.connectgaps,
213214
baseTolerance: Math.max(line.width || 1, 3) / 4,
214215
shape: line.shape,

src/traces/violin/plot.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) {
1313
var xa = plotinfo.xaxis;
1414
var ya = plotinfo.yaxis;
1515

16-
function makePath(pts) {
16+
function makePath(pts, trace) {
1717
var segments = linePoints(pts, {
1818
xaxis: xa,
1919
yaxis: ya,
20+
trace: trace,
2021
connectGaps: true,
2122
baseTolerance: 0.75,
2223
shape: 'spline',
@@ -80,7 +81,7 @@ module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) {
8081
pt[t.posLetter] = posCenter + (density[i].v / scale);
8182
pt[t.valLetter] = valAxis.c2l(density[i].t, true);
8283
}
83-
pathPos = makePath(pts);
84+
pathPos = makePath(pts, trace);
8485
}
8586

8687
if(hasNegativeSide) {
@@ -90,7 +91,7 @@ module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) {
9091
pt[t.posLetter] = posCenter - (density[i].v / scale);
9192
pt[t.valLetter] = valAxis.c2l(density[i].t, true);
9293
}
93-
pathNeg = makePath(pts);
94+
pathNeg = makePath(pts, trace);
9495
}
9596

9697
if(hasBothSides) {

test/image/baselines/19.png

207 Bytes
Loading
-712 Bytes
Loading
Loading

test/image/baselines/smith_gaps.png

-170 Bytes
Loading
359 Bytes
Loading
-1.38 KB
Loading
Loading
-607 Bytes
Loading
224 Bytes
Loading
Loading

0 commit comments

Comments
 (0)