@@ -221,6 +221,7 @@ var SYMBOLDEFS = require('./symbol_defs');
221221
222222drawing . symbolNames = [ ] ;
223223drawing . symbolFuncs = [ ] ;
224+ drawing . symbolBackOffs = [ ] ;
224225drawing . symbolNeedLines = { } ;
225226drawing . symbolNoDot = { } ;
226227drawing . 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
295297var 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+
10831108function 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
11071132var 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} ;
11261161drawing . 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
11391228drawing . 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