Skip to content

Commit 578fc34

Browse files
authored
Merge pull request #6297 from plotly/marker-angle
Add `angle`, `angleref` and `standoff` to `marker` and add `backoff` to `line` as well as adding two new arrow symbols
2 parents 6f01227 + b4ef2ab commit 578fc34

File tree

91 files changed

+4581
-465
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+4581
-465
lines changed

src/components/drawing/index.js

+277-23
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;
@@ -287,9 +289,9 @@ drawing.symbolNumber = function(v) {
287289
0 : Math.floor(Math.max(v, 0));
288290
};
289291

290-
function makePointPath(symbolNumber, r) {
292+
function makePointPath(symbolNumber, r, t, s) {
291293
var base = symbolNumber % 100;
292-
return drawing.symbolFuncs[base](r) + (symbolNumber >= 200 ? DOTPATH : '');
294+
return drawing.symbolFuncs[base](r, t, s) + (symbolNumber >= 200 ? DOTPATH : '');
293295
}
294296

295297
var HORZGRADIENT = {x1: 1, x2: 0, y1: 0, y2: 0};
@@ -660,7 +662,10 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) {
660662
// because that impacts how to handle colors
661663
d.om = x % 200 >= 100;
662664

663-
sel.attr('d', makePointPath(x, r));
665+
var angle = getMarkerAngle(d, trace);
666+
var standoff = getMarkerStandoff(d, trace);
667+
668+
sel.attr('d', makePointPath(x, r, angle, standoff));
664669
}
665670

666671
var perPointGradient = false;
@@ -909,7 +914,7 @@ drawing.selectedPointStyle = function(s, trace) {
909914
var mx = d.mx || marker.symbol || 0;
910915
var mrc2 = fns.selectedSizeFn(d);
911916

912-
pt.attr('d', makePointPath(drawing.symbolNumber(mx), mrc2));
917+
pt.attr('d', makePointPath(drawing.symbolNumber(mx), mrc2, getMarkerAngle(d, trace), getMarkerStandoff(d, trace)));
913918

914919
// save for Drawing.selectedTextStyle
915920
d.mrc2 = mrc2;
@@ -1080,6 +1085,26 @@ drawing.smoothclosed = function(pts, smoothness) {
10801085
return path;
10811086
};
10821087

1088+
var lastDrawnX, lastDrawnY;
1089+
1090+
function roundEnd(pt, isY, isLastPoint) {
1091+
if(isLastPoint) pt = applyBackoff(pt);
1092+
1093+
return isY ? roundY(pt[1]) : roundX(pt[0]);
1094+
}
1095+
1096+
function roundX(p) {
1097+
var v = d3.round(p, 2);
1098+
lastDrawnX = v;
1099+
return v;
1100+
}
1101+
1102+
function roundY(p) {
1103+
var v = d3.round(p, 2);
1104+
lastDrawnY = v;
1105+
return v;
1106+
}
1107+
10831108
function makeTangent(prevpt, thispt, nextpt, smoothness) {
10841109
var d1x = prevpt[0] - thispt[0];
10851110
var d1y = prevpt[1] - thispt[1];
@@ -1093,47 +1118,111 @@ function makeTangent(prevpt, thispt, nextpt, smoothness) {
10931118
var denom2 = 3 * d1a * (d1a + d2a);
10941119
return [
10951120
[
1096-
d3.round(thispt[0] + (denom1 && numx / denom1), 2),
1097-
d3.round(thispt[1] + (denom1 && numy / denom1), 2)
1121+
roundX(thispt[0] + (denom1 && numx / denom1)),
1122+
roundY(thispt[1] + (denom1 && numy / denom1))
10981123
], [
1099-
d3.round(thispt[0] - (denom2 && numx / denom2), 2),
1100-
d3.round(thispt[1] - (denom2 && numy / denom2), 2)
1124+
roundX(thispt[0] - (denom2 && numx / denom2)),
1125+
roundY(thispt[1] - (denom2 && numy / denom2))
11011126
]
11021127
];
11031128
}
11041129

11051130
// step paths - returns a generator function for paths
11061131
// with the given step shape
11071132
var STEPPATH = {
1108-
hv: function(p0, p1) {
1109-
return 'H' + d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2);
1133+
hv: function(p0, p1, isLastPoint) {
1134+
return 'H' +
1135+
roundX(p1[0]) + 'V' +
1136+
roundEnd(p1, 1, isLastPoint);
11101137
},
1111-
vh: function(p0, p1) {
1112-
return 'V' + d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2);
1138+
vh: function(p0, p1, isLastPoint) {
1139+
return 'V' +
1140+
roundY(p1[1]) + 'H' +
1141+
roundEnd(p1, 0, isLastPoint);
11131142
},
1114-
hvh: function(p0, p1) {
1115-
return 'H' + d3.round((p0[0] + p1[0]) / 2, 2) + 'V' +
1116-
d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2);
1143+
hvh: function(p0, p1, isLastPoint) {
1144+
return 'H' +
1145+
roundX((p0[0] + p1[0]) / 2) + 'V' +
1146+
roundY(p1[1]) + 'H' +
1147+
roundEnd(p1, 0, isLastPoint);
11171148
},
1118-
vhv: function(p0, p1) {
1119-
return 'V' + d3.round((p0[1] + p1[1]) / 2, 2) + 'H' +
1120-
d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2);
1149+
vhv: function(p0, p1, isLastPoint) {
1150+
return 'V' +
1151+
roundY((p0[1] + p1[1]) / 2) + 'H' +
1152+
roundX(p1[0]) + 'V' +
1153+
roundEnd(p1, 1, isLastPoint);
11211154
}
11221155
};
1123-
var STEPLINEAR = function(p0, p1) {
1124-
return 'L' + d3.round(p1[0], 2) + ',' + d3.round(p1[1], 2);
1156+
var STEPLINEAR = function(p0, p1, isLastPoint) {
1157+
return 'L' +
1158+
roundEnd(p1, 0, isLastPoint) + ',' +
1159+
roundEnd(p1, 1, isLastPoint);
11251160
};
11261161
drawing.steps = function(shape) {
11271162
var onestep = STEPPATH[shape] || STEPLINEAR;
11281163
return function(pts) {
1129-
var path = 'M' + d3.round(pts[0][0], 2) + ',' + d3.round(pts[0][1], 2);
1130-
for(var i = 1; i < pts.length; i++) {
1131-
path += onestep(pts[i - 1], pts[i]);
1164+
var path = 'M' + roundX(pts[0][0]) + ',' + roundY(pts[0][1]);
1165+
var len = pts.length;
1166+
for(var i = 1; i < len; i++) {
1167+
path += onestep(pts[i - 1], pts[i], i === len - 1);
11321168
}
11331169
return path;
11341170
};
11351171
};
11361172

1173+
function applyBackoff(pt, start) {
1174+
var backoff = pt.backoff;
1175+
var trace = pt.trace;
1176+
var d = pt.d;
1177+
var i = pt.i;
1178+
1179+
if(backoff && trace &&
1180+
trace.marker &&
1181+
trace.marker.angle % 360 === 0 &&
1182+
trace.line &&
1183+
trace.line.shape !== 'spline'
1184+
) {
1185+
var arrayBackoff = Lib.isArrayOrTypedArray(backoff);
1186+
var end = pt;
1187+
1188+
var x1 = start ? start[0] : lastDrawnX || 0;
1189+
var y1 = start ? start[1] : lastDrawnY || 0;
1190+
1191+
var x2 = end[0];
1192+
var y2 = end[1];
1193+
1194+
var dx = x2 - x1;
1195+
var dy = y2 - y1;
1196+
1197+
var t = Math.atan2(dy, dx);
1198+
1199+
var b = arrayBackoff ? backoff[i] : backoff;
1200+
1201+
if(b === 'auto') {
1202+
var endI = end.i;
1203+
if(trace.type === 'scatter') endI--; // Why we need this hack?
1204+
1205+
var endMarker = end.marker;
1206+
b = endMarker ? drawing.symbolBackOffs[drawing.symbolNumber(endMarker.symbol)] * endMarker.size : 0;
1207+
b += drawing.getMarkerStandoff(d[endI], trace) || 0;
1208+
}
1209+
1210+
var x = x2 - b * Math.cos(t);
1211+
var y = y2 - b * Math.sin(t);
1212+
1213+
if(
1214+
((x <= x2 && x >= x1) || (x >= x2 && x <= x1)) &&
1215+
((y <= y2 && y >= y1) || (y >= y2 && y <= y1))
1216+
) {
1217+
pt = [x, y];
1218+
}
1219+
}
1220+
1221+
return pt;
1222+
}
1223+
1224+
drawing.applyBackoff = applyBackoff;
1225+
11371226
// off-screen svg render testing element, shared by the whole page
11381227
// uses the id 'js-plotly-tester' and stores it in drawing.tester
11391228
drawing.makeTester = function() {
@@ -1458,3 +1547,168 @@ drawing.setTextPointsScale = function(selection, xScale, yScale) {
14581547
el.attr('transform', transforms.join(''));
14591548
});
14601549
};
1550+
1551+
function getMarkerStandoff(d, trace) {
1552+
var standoff;
1553+
1554+
if(d) standoff = d.mf;
1555+
1556+
if(standoff === undefined) {
1557+
standoff = trace.marker ? trace.marker.standoff || 0 : 0;
1558+
}
1559+
1560+
if(!trace._geo && !trace._xA) {
1561+
// case of legends
1562+
return -standoff;
1563+
}
1564+
1565+
return standoff;
1566+
}
1567+
1568+
drawing.getMarkerStandoff = getMarkerStandoff;
1569+
1570+
var atan2 = Math.atan2;
1571+
var cos = Math.cos;
1572+
var sin = Math.sin;
1573+
1574+
function rotate(t, xy) {
1575+
var x = xy[0];
1576+
var y = xy[1];
1577+
return [
1578+
x * cos(t) - y * sin(t),
1579+
x * sin(t) + y * cos(t)
1580+
];
1581+
}
1582+
1583+
var previousLon;
1584+
var previousLat;
1585+
var previousX;
1586+
var previousY;
1587+
var previousI;
1588+
var previousTraceUid;
1589+
1590+
function getMarkerAngle(d, trace) {
1591+
var angle = d.ma;
1592+
1593+
if(angle === undefined) {
1594+
angle = trace.marker.angle || 0;
1595+
}
1596+
1597+
var x, y;
1598+
var ref = trace.marker.angleref;
1599+
if(ref === 'previous' || ref === 'north') {
1600+
if(trace._geo) {
1601+
var p = trace._geo.project(d.lonlat);
1602+
x = p[0];
1603+
y = p[1];
1604+
} else {
1605+
var xa = trace._xA;
1606+
var ya = trace._yA;
1607+
if(xa && ya) {
1608+
x = xa.c2p(d.x);
1609+
y = ya.c2p(d.y);
1610+
} else {
1611+
// case of legends
1612+
return 90;
1613+
}
1614+
}
1615+
1616+
if(trace._geo) {
1617+
var lon = d.lonlat[0];
1618+
var lat = d.lonlat[1];
1619+
1620+
var north = trace._geo.project([
1621+
lon,
1622+
lat + 1e-5 // epsilon
1623+
]);
1624+
1625+
var east = trace._geo.project([
1626+
lon + 1e-5, // epsilon
1627+
lat
1628+
]);
1629+
1630+
var u = atan2(
1631+
east[1] - y,
1632+
east[0] - x
1633+
);
1634+
1635+
var v = atan2(
1636+
north[1] - y,
1637+
north[0] - x
1638+
);
1639+
1640+
var t;
1641+
if(ref === 'north') {
1642+
t = angle / 180 * Math.PI;
1643+
// To use counter-clockwise angles i.e.
1644+
// East: 90, West: -90
1645+
// to facilitate wind visualisations
1646+
// in future we should use t = -t here.
1647+
} else if(ref === 'previous') {
1648+
var lon1 = lon / 180 * Math.PI;
1649+
var lat1 = lat / 180 * Math.PI;
1650+
var lon2 = previousLon / 180 * Math.PI;
1651+
var lat2 = previousLat / 180 * Math.PI;
1652+
1653+
var dLon = lon2 - lon1;
1654+
1655+
var deltaY = cos(lat2) * sin(dLon);
1656+
var deltaX = sin(lat2) * cos(lat1) - cos(lat2) * sin(lat1) * cos(dLon);
1657+
1658+
t = -atan2(
1659+
deltaY,
1660+
deltaX
1661+
) - Math.PI;
1662+
1663+
previousLon = lon;
1664+
previousLat = lat;
1665+
}
1666+
1667+
var A = rotate(u, [cos(t), 0]);
1668+
var B = rotate(v, [sin(t), 0]);
1669+
1670+
angle = atan2(
1671+
A[1] + B[1],
1672+
A[0] + B[0]
1673+
) / Math.PI * 180;
1674+
1675+
if(ref === 'previous' && !(
1676+
previousTraceUid === trace.uid &&
1677+
d.i === previousI + 1
1678+
)) {
1679+
angle = null;
1680+
}
1681+
}
1682+
1683+
if(ref === 'previous' && !trace._geo) {
1684+
if(
1685+
previousTraceUid === trace.uid &&
1686+
d.i === previousI + 1 &&
1687+
isNumeric(x) &&
1688+
isNumeric(y)
1689+
) {
1690+
var dX = x - previousX;
1691+
var dY = y - previousY;
1692+
1693+
var shape = trace.line ? trace.line.shape || '' : '';
1694+
1695+
var lastShapeChar = shape.slice(shape.length - 1);
1696+
if(lastShapeChar === 'h') dY = 0;
1697+
if(lastShapeChar === 'v') dX = 0;
1698+
1699+
angle += atan2(dY, dX) / Math.PI * 180 + 90;
1700+
} else {
1701+
angle = null;
1702+
}
1703+
}
1704+
}
1705+
1706+
previousX = x;
1707+
previousY = y;
1708+
previousI = d.i;
1709+
previousTraceUid = trace.uid;
1710+
1711+
return angle;
1712+
}
1713+
1714+
drawing.getMarkerAngle = getMarkerAngle;

0 commit comments

Comments
 (0)