pathfinder.src.js 77 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336
  1. /**
  2. * @license Highcharts JS v7.0.2 (2019-01-17)
  3. * Pathfinder
  4. *
  5. * (c) 2016-2019 Øystein Moseng
  6. *
  7. * License: www.highcharts.com/license
  8. */
  9. 'use strict';
  10. (function (factory) {
  11. if (typeof module === 'object' && module.exports) {
  12. factory['default'] = factory;
  13. module.exports = factory;
  14. } else if (typeof define === 'function' && define.amd) {
  15. define(function () {
  16. return factory;
  17. });
  18. } else {
  19. factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined);
  20. }
  21. }(function (Highcharts) {
  22. var algorithms = (function (H) {
  23. /* *
  24. * (c) 2016 Highsoft AS
  25. * Author: Øystein Moseng
  26. *
  27. * License: www.highcharts.com/license
  28. */
  29. var min = Math.min,
  30. max = Math.max,
  31. abs = Math.abs,
  32. pick = H.pick;
  33. /**
  34. * Get index of last obstacle before xMin. Employs a type of binary search, and
  35. * thus requires that obstacles are sorted by xMin value.
  36. *
  37. * @private
  38. * @function findLastObstacleBefore
  39. *
  40. * @param {Array<object>} obstacles
  41. * Array of obstacles to search in.
  42. *
  43. * @param {number} xMin
  44. * The xMin threshold.
  45. *
  46. * @param {number} startIx
  47. * Starting index to search from. Must be within array range.
  48. *
  49. * @return {number}
  50. * The index of the last obstacle element before xMin.
  51. */
  52. function findLastObstacleBefore(obstacles, xMin, startIx) {
  53. var left = startIx || 0, // left limit
  54. right = obstacles.length - 1, // right limit
  55. min = xMin - 0.0000001, // Make sure we include all obstacles at xMin
  56. cursor,
  57. cmp;
  58. while (left <= right) {
  59. cursor = (right + left) >> 1;
  60. cmp = min - obstacles[cursor].xMin;
  61. if (cmp > 0) {
  62. left = cursor + 1;
  63. } else if (cmp < 0) {
  64. right = cursor - 1;
  65. } else {
  66. return cursor;
  67. }
  68. }
  69. return left > 0 ? left - 1 : 0;
  70. }
  71. /**
  72. * Test if a point lays within an obstacle.
  73. *
  74. * @private
  75. * @function pointWithinObstacle
  76. *
  77. * @param {object} obstacle
  78. * Obstacle to test.
  79. *
  80. * @param {Highcharts.Point} point
  81. * Point with x/y props.
  82. *
  83. * @return {boolean}
  84. * Whether point is within the obstacle or not.
  85. */
  86. function pointWithinObstacle(obstacle, point) {
  87. return (
  88. point.x <= obstacle.xMax &&
  89. point.x >= obstacle.xMin &&
  90. point.y <= obstacle.yMax &&
  91. point.y >= obstacle.yMin
  92. );
  93. }
  94. /**
  95. * Find the index of an obstacle that wraps around a point.
  96. * Returns -1 if not found.
  97. *
  98. * @private
  99. * @function findObstacleFromPoint
  100. *
  101. * @param {Array<object>} obstacles
  102. * Obstacles to test.
  103. *
  104. * @param {Highcharts.Point} point
  105. * Point with x/y props.
  106. *
  107. * @return {number}
  108. * Ix of the obstacle in the array, or -1 if not found.
  109. */
  110. function findObstacleFromPoint(obstacles, point) {
  111. var i = findLastObstacleBefore(obstacles, point.x + 1) + 1;
  112. while (i--) {
  113. if (obstacles[i].xMax >= point.x &&
  114. // optimization using lazy evaluation
  115. pointWithinObstacle(obstacles[i], point)) {
  116. return i;
  117. }
  118. }
  119. return -1;
  120. }
  121. /**
  122. * Get SVG path array from array of line segments.
  123. *
  124. * @private
  125. * @function pathFromSegments
  126. *
  127. * @param {Array<object>} segments
  128. * The segments to build the path from.
  129. *
  130. * @return {Highcharts.SVGPathArray}
  131. * SVG path array as accepted by the SVG Renderer.
  132. */
  133. function pathFromSegments(segments) {
  134. var path = [];
  135. if (segments.length) {
  136. path.push('M', segments[0].start.x, segments[0].start.y);
  137. for (var i = 0; i < segments.length; ++i) {
  138. path.push('L', segments[i].end.x, segments[i].end.y);
  139. }
  140. }
  141. return path;
  142. }
  143. /**
  144. * Limits obstacle max/mins in all directions to bounds. Modifies input
  145. * obstacle.
  146. *
  147. * @private
  148. * @function limitObstacleToBounds
  149. *
  150. * @param {object} obstacle
  151. * Obstacle to limit.
  152. *
  153. * @param {object} bounds
  154. * Bounds to use as limit.
  155. */
  156. function limitObstacleToBounds(obstacle, bounds) {
  157. obstacle.yMin = max(obstacle.yMin, bounds.yMin);
  158. obstacle.yMax = min(obstacle.yMax, bounds.yMax);
  159. obstacle.xMin = max(obstacle.xMin, bounds.xMin);
  160. obstacle.xMax = min(obstacle.xMax, bounds.xMax);
  161. }
  162. // Define the available pathfinding algorithms.
  163. // Algorithms take up to 3 arguments: starting point, ending point, and an
  164. // options object.
  165. var algorithms = {
  166. /**
  167. * Get an SVG path from a starting coordinate to an ending coordinate.
  168. * Draws a straight line.
  169. *
  170. * @function Highcharts.Pathfinder.algorithms.straight
  171. *
  172. * @param {object} start
  173. * Starting coordinate, object with x/y props.
  174. *
  175. * @param {object} end
  176. * Ending coordinate, object with x/y props.
  177. *
  178. * @return {object}
  179. * An object with the SVG path in Array form as accepted by the SVG
  180. * renderer, as well as an array of new obstacles making up this
  181. * path.
  182. */
  183. straight: function (start, end) {
  184. return {
  185. path: ['M', start.x, start.y, 'L', end.x, end.y],
  186. obstacles: [{ start: start, end: end }]
  187. };
  188. },
  189. /**
  190. * Find a path from a starting coordinate to an ending coordinate, using
  191. * right angles only, and taking only starting/ending obstacle into
  192. * consideration.
  193. *
  194. * @function Highcharts.Pathfinder.algorithms.simpleConnect
  195. *
  196. * @param {object} start
  197. * Starting coordinate, object with x/y props.
  198. *
  199. * @param {object} end
  200. * Ending coordinate, object with x/y props.
  201. *
  202. * @param {object} options
  203. * Options for the algorithm:
  204. * - chartObstacles: Array of chart obstacles to avoid
  205. * - startDirectionX: Optional. True if starting in the X direction.
  206. * If not provided, the algorithm starts in the direction that is
  207. * the furthest between start/end.
  208. *
  209. * @return {object}
  210. * An object with the SVG path in Array form as accepted by the SVG
  211. * renderer, as well as an array of new obstacles making up this
  212. * path.
  213. */
  214. simpleConnect: H.extend(function (start, end, options) {
  215. var segments = [],
  216. endSegment,
  217. dir = pick(
  218. options.startDirectionX,
  219. abs(end.x - start.x) > abs(end.y - start.y)
  220. ) ? 'x' : 'y',
  221. chartObstacles = options.chartObstacles,
  222. startObstacleIx = findObstacleFromPoint(chartObstacles, start),
  223. endObstacleIx = findObstacleFromPoint(chartObstacles, end),
  224. startObstacle,
  225. endObstacle,
  226. prevWaypoint,
  227. waypoint,
  228. waypoint2,
  229. useMax,
  230. endPoint;
  231. // Return a clone of a point with a property set from a target object,
  232. // optionally with an offset
  233. function copyFromPoint(from, fromKey, to, toKey, offset) {
  234. var point = {
  235. x: from.x,
  236. y: from.y
  237. };
  238. point[fromKey] = to[toKey || fromKey] + (offset || 0);
  239. return point;
  240. }
  241. // Return waypoint outside obstacle
  242. function getMeOut(obstacle, point, direction) {
  243. var useMax = abs(point[direction] - obstacle[direction + 'Min']) >
  244. abs(point[direction] - obstacle[direction + 'Max']);
  245. return copyFromPoint(
  246. point,
  247. direction,
  248. obstacle,
  249. direction + (useMax ? 'Max' : 'Min'),
  250. useMax ? 1 : -1
  251. );
  252. }
  253. // Pull out end point
  254. if (endObstacleIx > -1) {
  255. endObstacle = chartObstacles[endObstacleIx];
  256. waypoint = getMeOut(endObstacle, end, dir);
  257. endSegment = {
  258. start: waypoint,
  259. end: end
  260. };
  261. endPoint = waypoint;
  262. } else {
  263. endPoint = end;
  264. }
  265. // If an obstacle envelops the start point, add a segment to get out,
  266. // and around it.
  267. if (startObstacleIx > -1) {
  268. startObstacle = chartObstacles[startObstacleIx];
  269. waypoint = getMeOut(startObstacle, start, dir);
  270. segments.push({
  271. start: start,
  272. end: waypoint
  273. });
  274. // If we are going back again, switch direction to get around start
  275. // obstacle.
  276. if (
  277. waypoint[dir] > start[dir] === // Going towards max from start
  278. waypoint[dir] > endPoint[dir] // Going towards min to end
  279. ) {
  280. dir = dir === 'y' ? 'x' : 'y';
  281. useMax = start[dir] < end[dir];
  282. segments.push({
  283. start: waypoint,
  284. end: copyFromPoint(
  285. waypoint,
  286. dir,
  287. startObstacle,
  288. dir + (useMax ? 'Max' : 'Min'),
  289. useMax ? 1 : -1
  290. )
  291. });
  292. // Switch direction again
  293. dir = dir === 'y' ? 'x' : 'y';
  294. }
  295. }
  296. // We are around the start obstacle. Go towards the end in one
  297. // direction.
  298. prevWaypoint = segments.length ?
  299. segments[segments.length - 1].end :
  300. start;
  301. waypoint = copyFromPoint(prevWaypoint, dir, endPoint);
  302. segments.push({
  303. start: prevWaypoint,
  304. end: waypoint
  305. });
  306. // Final run to end point in the other direction
  307. dir = dir === 'y' ? 'x' : 'y';
  308. waypoint2 = copyFromPoint(waypoint, dir, endPoint);
  309. segments.push({
  310. start: waypoint,
  311. end: waypoint2
  312. });
  313. // Finally add the endSegment
  314. segments.push(endSegment);
  315. return {
  316. path: pathFromSegments(segments),
  317. obstacles: segments
  318. };
  319. }, {
  320. requiresObstacles: true
  321. }),
  322. /**
  323. * Find a path from a starting coordinate to an ending coordinate, taking
  324. * obstacles into consideration. Might not always find the optimal path,
  325. * but is fast, and usually good enough.
  326. *
  327. * @function Highcharts.Pathfinder.algorithms.fastAvoid
  328. *
  329. * @param {object} start
  330. * Starting coordinate, object with x/y props.
  331. *
  332. * @param {object} end
  333. * Ending coordinate, object with x/y props.
  334. *
  335. * @param {object} options
  336. * Options for the algorithm.
  337. * - chartObstacles: Array of chart obstacles to avoid
  338. * - lineObstacles: Array of line obstacles to jump over
  339. * - obstacleMetrics: Object with metrics of chartObstacles cached
  340. * - hardBounds: Hard boundaries to not cross
  341. * - obstacleOptions: Options for the obstacles, including margin
  342. * - startDirectionX: Optional. True if starting in the X direction.
  343. * If not provided, the algorithm starts in the
  344. * direction that is the furthest between
  345. * start/end.
  346. *
  347. * @return {object}
  348. * An object with the SVG path in Array form as accepted by the SVG
  349. * renderer, as well as an array of new obstacles making up this
  350. * path.
  351. */
  352. fastAvoid: H.extend(function (start, end, options) {
  353. /*
  354. Algorithm rules/description
  355. - Find initial direction
  356. - Determine soft/hard max for each direction.
  357. - Move along initial direction until obstacle.
  358. - Change direction.
  359. - If hitting obstacle, first try to change length of previous line
  360. before changing direction again.
  361. Soft min/max x = start/destination x +/- widest obstacle + margin
  362. Soft min/max y = start/destination y +/- tallest obstacle + margin
  363. @todo:
  364. - Make retrospective, try changing prev segment to reduce
  365. corners
  366. - Fix logic for breaking out of end-points - not always picking
  367. the best direction currently
  368. - When going around the end obstacle we should not always go the
  369. shortest route, rather pick the one closer to the end point
  370. */
  371. var dirIsX = pick(
  372. options.startDirectionX,
  373. abs(end.x - start.x) > abs(end.y - start.y)
  374. ),
  375. dir = dirIsX ? 'x' : 'y',
  376. segments,
  377. useMax,
  378. extractedEndPoint,
  379. endSegments = [],
  380. forceObstacleBreak = false, // Used in clearPathTo to keep track of
  381. // when to force break through an obstacle.
  382. // Boundaries to stay within. If beyond soft boundary, prefer to
  383. // change direction ASAP. If at hard max, always change immediately.
  384. metrics = options.obstacleMetrics,
  385. softMinX = min(start.x, end.x) - metrics.maxWidth - 10,
  386. softMaxX = max(start.x, end.x) + metrics.maxWidth + 10,
  387. softMinY = min(start.y, end.y) - metrics.maxHeight - 10,
  388. softMaxY = max(start.y, end.y) + metrics.maxHeight + 10,
  389. // Obstacles
  390. chartObstacles = options.chartObstacles,
  391. startObstacleIx = findLastObstacleBefore(chartObstacles, softMinX),
  392. endObstacleIx = findLastObstacleBefore(chartObstacles, softMaxX);
  393. // How far can you go between two points before hitting an obstacle?
  394. // Does not work for diagonal lines (because it doesn't have to).
  395. function pivotPoint(fromPoint, toPoint, directionIsX) {
  396. var firstPoint,
  397. lastPoint,
  398. highestPoint,
  399. lowestPoint,
  400. i,
  401. searchDirection = fromPoint.x < toPoint.x ? 1 : -1;
  402. if (fromPoint.x < toPoint.x) {
  403. firstPoint = fromPoint;
  404. lastPoint = toPoint;
  405. } else {
  406. firstPoint = toPoint;
  407. lastPoint = fromPoint;
  408. }
  409. if (fromPoint.y < toPoint.y) {
  410. lowestPoint = fromPoint;
  411. highestPoint = toPoint;
  412. } else {
  413. lowestPoint = toPoint;
  414. highestPoint = fromPoint;
  415. }
  416. // Go through obstacle range in reverse if toPoint is before
  417. // fromPoint in the X-dimension.
  418. i = searchDirection < 0 ?
  419. // Searching backwards, start at last obstacle before last point
  420. min(findLastObstacleBefore(chartObstacles, lastPoint.x),
  421. chartObstacles.length - 1) :
  422. // Forwards. Since we're not sorted by xMax, we have to look
  423. // at all obstacles.
  424. 0;
  425. // Go through obstacles in this X range
  426. while (chartObstacles[i] && (
  427. searchDirection > 0 && chartObstacles[i].xMin <= lastPoint.x ||
  428. searchDirection < 0 && chartObstacles[i].xMax >= firstPoint.x
  429. )) {
  430. // If this obstacle is between from and to points in a straight
  431. // line, pivot at the intersection.
  432. if (
  433. chartObstacles[i].xMin <= lastPoint.x &&
  434. chartObstacles[i].xMax >= firstPoint.x &&
  435. chartObstacles[i].yMin <= highestPoint.y &&
  436. chartObstacles[i].yMax >= lowestPoint.y
  437. ) {
  438. if (directionIsX) {
  439. return {
  440. y: fromPoint.y,
  441. x: fromPoint.x < toPoint.x ?
  442. chartObstacles[i].xMin - 1 :
  443. chartObstacles[i].xMax + 1,
  444. obstacle: chartObstacles[i]
  445. };
  446. }
  447. // else ...
  448. return {
  449. x: fromPoint.x,
  450. y: fromPoint.y < toPoint.y ?
  451. chartObstacles[i].yMin - 1 :
  452. chartObstacles[i].yMax + 1,
  453. obstacle: chartObstacles[i]
  454. };
  455. }
  456. i += searchDirection;
  457. }
  458. return toPoint;
  459. }
  460. /**
  461. * Decide in which direction to dodge or get out of an obstacle.
  462. * Considers desired direction, which way is shortest, soft and hard
  463. * bounds.
  464. *
  465. * (? Returns a string, either xMin, xMax, yMin or yMax.)
  466. *
  467. * @private
  468. * @function
  469. *
  470. * @param {object} obstacle
  471. * Obstacle to dodge/escape.
  472. *
  473. * @param {object} fromPoint
  474. * Point with x/y props that's dodging/escaping.
  475. *
  476. * @param {object} toPoint
  477. * Goal point.
  478. *
  479. * @param {boolean} dirIsX
  480. * Dodge in X dimension.
  481. *
  482. * @param {object} bounds
  483. * Hard and soft boundaries.
  484. *
  485. * @return {boolean}
  486. * Use max or not.
  487. */
  488. function getDodgeDirection(
  489. obstacle,
  490. fromPoint,
  491. toPoint,
  492. dirIsX,
  493. bounds
  494. ) {
  495. var softBounds = bounds.soft,
  496. hardBounds = bounds.hard,
  497. dir = dirIsX ? 'x' : 'y',
  498. toPointMax = { x: fromPoint.x, y: fromPoint.y },
  499. toPointMin = { x: fromPoint.x, y: fromPoint.y },
  500. minPivot,
  501. maxPivot,
  502. maxOutOfSoftBounds = obstacle[dir + 'Max'] >=
  503. softBounds[dir + 'Max'],
  504. minOutOfSoftBounds = obstacle[dir + 'Min'] <=
  505. softBounds[dir + 'Min'],
  506. maxOutOfHardBounds = obstacle[dir + 'Max'] >=
  507. hardBounds[dir + 'Max'],
  508. minOutOfHardBounds = obstacle[dir + 'Min'] <=
  509. hardBounds[dir + 'Min'],
  510. // Find out if we should prefer one direction over the other if
  511. // we can choose freely
  512. minDistance = abs(obstacle[dir + 'Min'] - fromPoint[dir]),
  513. maxDistance = abs(obstacle[dir + 'Max'] - fromPoint[dir]),
  514. // If it's a small difference, pick the one leading towards dest
  515. // point. Otherwise pick the shortest distance
  516. useMax = abs(minDistance - maxDistance) < 10 ?
  517. fromPoint[dir] < toPoint[dir] :
  518. maxDistance < minDistance;
  519. // Check if we hit any obstacles trying to go around in either
  520. // direction.
  521. toPointMin[dir] = obstacle[dir + 'Min'];
  522. toPointMax[dir] = obstacle[dir + 'Max'];
  523. minPivot = pivotPoint(fromPoint, toPointMin, dirIsX)[dir] !==
  524. toPointMin[dir];
  525. maxPivot = pivotPoint(fromPoint, toPointMax, dirIsX)[dir] !==
  526. toPointMax[dir];
  527. useMax = minPivot ?
  528. (maxPivot ? useMax : true) :
  529. (maxPivot ? false : useMax);
  530. // useMax now contains our preferred choice, bounds not taken into
  531. // account. If both or neither direction is out of bounds we want to
  532. // use this.
  533. // Deal with soft bounds
  534. useMax = minOutOfSoftBounds ?
  535. (maxOutOfSoftBounds ? useMax : true) : // Out on min
  536. (maxOutOfSoftBounds ? false : useMax); // Not out on min
  537. // Deal with hard bounds
  538. useMax = minOutOfHardBounds ?
  539. (maxOutOfHardBounds ? useMax : true) : // Out on min
  540. (maxOutOfHardBounds ? false : useMax); // Not out on min
  541. return useMax;
  542. }
  543. // Find a clear path between point
  544. function clearPathTo(fromPoint, toPoint, dirIsX) {
  545. // Don't waste time if we've hit goal
  546. if (fromPoint.x === toPoint.x && fromPoint.y === toPoint.y) {
  547. return [];
  548. }
  549. var dir = dirIsX ? 'x' : 'y',
  550. pivot,
  551. segments,
  552. waypoint,
  553. waypointUseMax,
  554. envelopingObstacle,
  555. secondEnvelopingObstacle,
  556. envelopWaypoint,
  557. obstacleMargin = options.obstacleOptions.margin,
  558. bounds = {
  559. soft: {
  560. xMin: softMinX,
  561. xMax: softMaxX,
  562. yMin: softMinY,
  563. yMax: softMaxY
  564. },
  565. hard: options.hardBounds
  566. };
  567. // If fromPoint is inside an obstacle we have a problem. Break out
  568. // by just going to the outside of this obstacle. We prefer to go to
  569. // the nearest edge in the chosen direction.
  570. envelopingObstacle =
  571. findObstacleFromPoint(chartObstacles, fromPoint);
  572. if (envelopingObstacle > -1) {
  573. envelopingObstacle = chartObstacles[envelopingObstacle];
  574. waypointUseMax = getDodgeDirection(
  575. envelopingObstacle, fromPoint, toPoint, dirIsX, bounds
  576. );
  577. // Cut obstacle to hard bounds to make sure we stay within
  578. limitObstacleToBounds(envelopingObstacle, options.hardBounds);
  579. envelopWaypoint = dirIsX ? {
  580. y: fromPoint.y,
  581. x: envelopingObstacle[waypointUseMax ? 'xMax' : 'xMin'] +
  582. (waypointUseMax ? 1 : -1)
  583. } : {
  584. x: fromPoint.x,
  585. y: envelopingObstacle[waypointUseMax ? 'yMax' : 'yMin'] +
  586. (waypointUseMax ? 1 : -1)
  587. };
  588. // If we crashed into another obstacle doing this, we put the
  589. // waypoint between them instead
  590. secondEnvelopingObstacle = findObstacleFromPoint(
  591. chartObstacles, envelopWaypoint
  592. );
  593. if (secondEnvelopingObstacle > -1) {
  594. secondEnvelopingObstacle = chartObstacles[
  595. secondEnvelopingObstacle
  596. ];
  597. // Cut obstacle to hard bounds
  598. limitObstacleToBounds(
  599. secondEnvelopingObstacle,
  600. options.hardBounds
  601. );
  602. // Modify waypoint to lay between obstacles
  603. envelopWaypoint[dir] = waypointUseMax ? max(
  604. envelopingObstacle[dir + 'Max'] - obstacleMargin + 1,
  605. (
  606. secondEnvelopingObstacle[dir + 'Min'] +
  607. envelopingObstacle[dir + 'Max']
  608. ) / 2
  609. ) :
  610. min((
  611. envelopingObstacle[dir + 'Min'] + obstacleMargin - 1
  612. ), (
  613. (
  614. secondEnvelopingObstacle[dir + 'Max'] +
  615. envelopingObstacle[dir + 'Min']
  616. ) / 2
  617. ));
  618. // We are not going anywhere. If this happens for the first
  619. // time, do nothing. Otherwise, try to go to the extreme of
  620. // the obstacle pair in the current direction.
  621. if (fromPoint.x === envelopWaypoint.x &&
  622. fromPoint.y === envelopWaypoint.y) {
  623. if (forceObstacleBreak) {
  624. envelopWaypoint[dir] = waypointUseMax ?
  625. max(
  626. envelopingObstacle[dir + 'Max'],
  627. secondEnvelopingObstacle[dir + 'Max']
  628. ) + 1 :
  629. min(
  630. envelopingObstacle[dir + 'Min'],
  631. secondEnvelopingObstacle[dir + 'Min']
  632. ) - 1;
  633. }
  634. // Toggle on if off, and the opposite
  635. forceObstacleBreak = !forceObstacleBreak;
  636. } else {
  637. // This point is not identical to previous.
  638. // Clear break trigger.
  639. forceObstacleBreak = false;
  640. }
  641. }
  642. segments = [{
  643. start: fromPoint,
  644. end: envelopWaypoint
  645. }];
  646. } else { // If not enveloping, use standard pivot calculation
  647. pivot = pivotPoint(fromPoint, {
  648. x: dirIsX ? toPoint.x : fromPoint.x,
  649. y: dirIsX ? fromPoint.y : toPoint.y
  650. }, dirIsX);
  651. segments = [{
  652. start: fromPoint,
  653. end: {
  654. x: pivot.x,
  655. y: pivot.y
  656. }
  657. }];
  658. // Pivot before goal, use a waypoint to dodge obstacle
  659. if (pivot[dirIsX ? 'x' : 'y'] !== toPoint[dirIsX ? 'x' : 'y']) {
  660. // Find direction of waypoint
  661. waypointUseMax = getDodgeDirection(
  662. pivot.obstacle, pivot, toPoint, !dirIsX, bounds
  663. );
  664. // Cut waypoint to hard bounds
  665. limitObstacleToBounds(pivot.obstacle, options.hardBounds);
  666. waypoint = {
  667. x: dirIsX ?
  668. pivot.x :
  669. pivot.obstacle[waypointUseMax ? 'xMax' : 'xMin'] +
  670. (waypointUseMax ? 1 : -1),
  671. y: dirIsX ?
  672. pivot.obstacle[waypointUseMax ? 'yMax' : 'yMin'] +
  673. (waypointUseMax ? 1 : -1) :
  674. pivot.y
  675. };
  676. // We're changing direction here, store that to make sure we
  677. // also change direction when adding the last segment array
  678. // after handling waypoint.
  679. dirIsX = !dirIsX;
  680. segments = segments.concat(clearPathTo({
  681. x: pivot.x,
  682. y: pivot.y
  683. }, waypoint, dirIsX));
  684. }
  685. }
  686. // Get segments for the other direction too
  687. // Recursion is our friend
  688. segments = segments.concat(clearPathTo(
  689. segments[segments.length - 1].end, toPoint, !dirIsX
  690. ));
  691. return segments;
  692. }
  693. // Extract point to outside of obstacle in whichever direction is
  694. // closest. Returns new point outside obstacle.
  695. function extractFromObstacle(obstacle, point, goalPoint) {
  696. var dirIsX = min(obstacle.xMax - point.x, point.x - obstacle.xMin) <
  697. min(obstacle.yMax - point.y, point.y - obstacle.yMin),
  698. bounds = {
  699. soft: options.hardBounds,
  700. hard: options.hardBounds
  701. },
  702. useMax = getDodgeDirection(
  703. obstacle, point, goalPoint, dirIsX, bounds
  704. );
  705. return dirIsX ? {
  706. y: point.y,
  707. x: obstacle[useMax ? 'xMax' : 'xMin'] + (useMax ? 1 : -1)
  708. } : {
  709. x: point.x,
  710. y: obstacle[useMax ? 'yMax' : 'yMin'] + (useMax ? 1 : -1)
  711. };
  712. }
  713. // Cut the obstacle array to soft bounds for optimization in large
  714. // datasets.
  715. chartObstacles =
  716. chartObstacles.slice(startObstacleIx, endObstacleIx + 1);
  717. // If an obstacle envelops the end point, move it out of there and add
  718. // a little segment to where it was.
  719. if ((endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1) {
  720. extractedEndPoint = extractFromObstacle(
  721. chartObstacles[endObstacleIx],
  722. end,
  723. start
  724. );
  725. endSegments.push({
  726. end: end,
  727. start: extractedEndPoint
  728. });
  729. end = extractedEndPoint;
  730. }
  731. // If it's still inside one or more obstacles, get out of there by
  732. // force-moving towards the start point.
  733. while (
  734. (endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1
  735. ) {
  736. useMax = end[dir] - start[dir] < 0;
  737. extractedEndPoint = {
  738. x: end.x,
  739. y: end.y
  740. };
  741. extractedEndPoint[dir] = chartObstacles[endObstacleIx][
  742. useMax ? dir + 'Max' : dir + 'Min'
  743. ] + (useMax ? 1 : -1);
  744. endSegments.push({
  745. end: end,
  746. start: extractedEndPoint
  747. });
  748. end = extractedEndPoint;
  749. }
  750. // Find the path
  751. segments = clearPathTo(start, end, dirIsX);
  752. // Add the end-point segments
  753. segments = segments.concat(endSegments.reverse());
  754. return {
  755. path: pathFromSegments(segments),
  756. obstacles: segments
  757. };
  758. }, {
  759. requiresObstacles: true
  760. })
  761. };
  762. return algorithms;
  763. }(Highcharts));
  764. (function (H) {
  765. /* *
  766. * (c) 2017 Highsoft AS
  767. * Authors: Lars A. V. Cabrera
  768. *
  769. * License: www.highcharts.com/license
  770. */
  771. /**
  772. * Creates an arrow symbol. Like a triangle, except not filled.
  773. * ```
  774. * o
  775. * o
  776. * o
  777. * o
  778. * o
  779. * o
  780. * o
  781. * ```
  782. *
  783. * @private
  784. * @function
  785. *
  786. * @param {number} x
  787. * x position of the arrow
  788. *
  789. * @param {number} y
  790. * y position of the arrow
  791. *
  792. * @param {number} w
  793. * width of the arrow
  794. *
  795. * @param {number} h
  796. * height of the arrow
  797. *
  798. * @return {Highcharts.SVGPathArray}
  799. * Path array
  800. */
  801. H.SVGRenderer.prototype.symbols.arrow = function (x, y, w, h) {
  802. return [
  803. 'M', x, y + h / 2,
  804. 'L', x + w, y,
  805. 'L', x, y + h / 2,
  806. 'L', x + w, y + h
  807. ];
  808. };
  809. /**
  810. * Creates a half-width arrow symbol. Like a triangle, except not filled.
  811. * ```
  812. * o
  813. * o
  814. * o
  815. * o
  816. * o
  817. * ```
  818. *
  819. * @private
  820. * @function
  821. *
  822. * @param {number} x
  823. * x position of the arrow
  824. *
  825. * @param {number} y
  826. * y position of the arrow
  827. *
  828. * @param {number} w
  829. * width of the arrow
  830. *
  831. * @param {number} h
  832. * height of the arrow
  833. *
  834. * @return {Highcharts.SVGPathArray}
  835. * Path array
  836. */
  837. H.SVGRenderer.prototype.symbols['arrow-half'] = function (x, y, w, h) {
  838. return H.SVGRenderer.prototype.symbols.arrow(x, y, w / 2, h);
  839. };
  840. /**
  841. * Creates a left-oriented triangle.
  842. * ```
  843. * o
  844. * ooooooo
  845. * ooooooooooooo
  846. * ooooooo
  847. * o
  848. * ```
  849. *
  850. * @private
  851. * @function
  852. *
  853. * @param {number} x
  854. * x position of the triangle
  855. *
  856. * @param {number} y
  857. * y position of the triangle
  858. *
  859. * @param {number} w
  860. * width of the triangle
  861. *
  862. * @param {number} h
  863. * height of the triangle
  864. *
  865. * @return {Highcharts.SVGPathArray}
  866. * Path array
  867. */
  868. H.SVGRenderer.prototype.symbols['triangle-left'] = function (x, y, w, h) {
  869. return [
  870. 'M', x + w, y,
  871. 'L', x, y + h / 2,
  872. 'L', x + w, y + h,
  873. 'Z'
  874. ];
  875. };
  876. /**
  877. * Alias function for triangle-left.
  878. *
  879. * @private
  880. * @function
  881. *
  882. * @param {number} x
  883. * x position of the arrow
  884. *
  885. * @param {number} y
  886. * y position of the arrow
  887. *
  888. * @param {number} w
  889. * width of the arrow
  890. *
  891. * @param {number} h
  892. * height of the arrow
  893. *
  894. * @return {Highcharts.SVGPathArray}
  895. * Path array
  896. */
  897. H.SVGRenderer.prototype.symbols['arrow-filled'] =
  898. H.SVGRenderer.prototype.symbols['triangle-left'];
  899. /**
  900. * Creates a half-width, left-oriented triangle.
  901. * ```
  902. * o
  903. * oooo
  904. * ooooooo
  905. * oooo
  906. * o
  907. * ```
  908. *
  909. * @private
  910. * @function
  911. *
  912. * @param {number} x
  913. * x position of the triangle
  914. *
  915. * @param {number} y
  916. * y position of the triangle
  917. *
  918. * @param {number} w
  919. * width of the triangle
  920. *
  921. * @param {number} h
  922. * height of the triangle
  923. *
  924. * @return {Highcharts.SVGPathArray}
  925. * Path array
  926. */
  927. H.SVGRenderer.prototype.symbols['triangle-left-half'] = function (x, y, w, h) {
  928. return H.SVGRenderer.prototype.symbols['triangle-left'](x, y, w / 2, h);
  929. };
  930. /**
  931. * Alias function for triangle-left-half.
  932. *
  933. * @private
  934. * @function
  935. *
  936. * @param {number} x
  937. * x position of the arrow
  938. *
  939. * @param {number} y
  940. * y position of the arrow
  941. *
  942. * @param {number} w
  943. * width of the arrow
  944. *
  945. * @param {number} h
  946. * height of the arrow
  947. *
  948. * @return {Highcharts.SVGPathArray}
  949. * Path array
  950. */
  951. H.SVGRenderer.prototype.symbols['arrow-filled-half'] =
  952. H.SVGRenderer.prototype.symbols['triangle-left-half'];
  953. }(Highcharts));
  954. (function (H, pathfinderAlgorithms) {
  955. /* *
  956. * (c) 2016 Highsoft AS
  957. * Authors: Øystein Moseng, Lars A. V. Cabrera
  958. *
  959. * License: www.highcharts.com/license
  960. */
  961. var defined = H.defined,
  962. deg2rad = H.deg2rad,
  963. extend = H.extend,
  964. addEvent = H.addEvent,
  965. merge = H.merge,
  966. pick = H.pick,
  967. max = Math.max,
  968. min = Math.min;
  969. /*
  970. @todo:
  971. - Document how to write your own algorithms
  972. - Consider adding a Point.pathTo method that wraps creating a connection
  973. and rendering it
  974. */
  975. // Set default Pathfinder options
  976. extend(H.defaultOptions, {
  977. /**
  978. * The Pathfinder module allows you to define connections between any two
  979. * points, represented as lines - optionally with markers for the start
  980. * and/or end points. Multiple algorithms are available for calculating how
  981. * the connecting lines are drawn.
  982. *
  983. * Connector functionality requires Highcharts Gantt to be loaded. In Gantt
  984. * charts, the connectors are used to draw dependencies between tasks.
  985. *
  986. * @see [dependency](series.gantt.data.dependency)
  987. *
  988. * @sample gantt/pathfinder/demo
  989. * Pathfinder connections
  990. *
  991. * @product gantt
  992. * @optionparent connectors
  993. */
  994. connectors: {
  995. /**
  996. * Enable connectors for this chart. Requires Highcharts Gantt.
  997. *
  998. * @type {boolean}
  999. * @default true
  1000. * @since 6.2.0
  1001. * @apioption connectors.enabled
  1002. */
  1003. /**
  1004. * Set the default dash style for this chart's connecting lines.
  1005. *
  1006. * @type {string}
  1007. * @default solid
  1008. * @since 6.2.0
  1009. * @apioption connectors.dashStyle
  1010. */
  1011. /**
  1012. * Set the default color for this chart's Pathfinder connecting lines.
  1013. * Defaults to the color of the point being connected.
  1014. *
  1015. * @type {Highcharts.ColorString}
  1016. * @since 6.2.0
  1017. * @apioption connectors.lineColor
  1018. */
  1019. /**
  1020. * Set the default pathfinder margin to use, in pixels. Some Pathfinder
  1021. * algorithms attempt to avoid obstacles, such as other points in the
  1022. * chart. These algorithms use this margin to determine how close lines
  1023. * can be to an obstacle. The default is to compute this automatically
  1024. * from the size of the obstacles in the chart.
  1025. *
  1026. * To draw connecting lines close to existing points, set this to a low
  1027. * number. For more space around existing points, set this number
  1028. * higher.
  1029. *
  1030. * @sample gantt/pathfinder/algorithm-margin
  1031. * Small algorithmMargin
  1032. *
  1033. * @type {number}
  1034. * @since 6.2.0
  1035. * @apioption connectors.algorithmMargin
  1036. */
  1037. /**
  1038. * Set the default pathfinder algorithm to use for this chart. It is
  1039. * possible to define your own algorithms by adding them to the
  1040. * Highcharts.Pathfinder.prototype.algorithms object before the chart
  1041. * has been created.
  1042. *
  1043. * The default algorithms are as follows:
  1044. *
  1045. * `straight`: Draws a straight line between the connecting
  1046. * points. Does not avoid other points when drawing.
  1047. *
  1048. * `simpleConnect`: Finds a path between the points using right angles
  1049. * only. Takes only starting/ending points into
  1050. * account, and will not avoid other points.
  1051. *
  1052. * `fastAvoid`: Finds a path between the points using right angles
  1053. * only. Will attempt to avoid other points, but its
  1054. * focus is performance over accuracy. Works well with
  1055. * less dense datasets.
  1056. *
  1057. * Default value: `straight` is used as default for most series types,
  1058. * while `simpleConnect` is used as default for Gantt series, to show
  1059. * dependencies between points.
  1060. *
  1061. * @sample gantt/pathfinder/demo
  1062. * Different types used
  1063. *
  1064. * @default undefined
  1065. * @since 6.2.0
  1066. * @validvalue ["straight", "simpleConnect", "fastAvoid"]
  1067. */
  1068. type: 'straight',
  1069. /**
  1070. * Set the default pixel width for this chart's Pathfinder connecting
  1071. * lines.
  1072. *
  1073. * @since 6.2.0
  1074. */
  1075. lineWidth: 1,
  1076. /**
  1077. * Marker options for this chart's Pathfinder connectors. Note that
  1078. * this option is overridden by the `startMarker` and `endMarker`
  1079. * options.
  1080. *
  1081. * @since 6.2.0
  1082. */
  1083. marker: {
  1084. /**
  1085. * Set the radius of the connector markers. The default is
  1086. * automatically computed based on the algorithmMargin setting.
  1087. *
  1088. * Setting marker.width and marker.height will override this
  1089. * setting.
  1090. *
  1091. * @type {number}
  1092. * @since 6.2.0
  1093. * @apioption connectors.marker.radius
  1094. */
  1095. /**
  1096. * Set the width of the connector markers. If not supplied, this
  1097. * is inferred from the marker radius.
  1098. *
  1099. * @type {number}
  1100. * @since 6.2.0
  1101. * @apioption connectors.marker.width
  1102. */
  1103. /**
  1104. * Set the height of the connector markers. If not supplied, this
  1105. * is inferred from the marker radius.
  1106. *
  1107. * @type {number}
  1108. * @since 6.2.0
  1109. * @apioption connectors.marker.height
  1110. */
  1111. /**
  1112. * Set the color of the connector markers. By default this is the
  1113. * same as the connector color.
  1114. *
  1115. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  1116. * @since 6.2.0
  1117. * @apioption connectors.marker.color
  1118. */
  1119. /**
  1120. * Set the line/border color of the connector markers. By default
  1121. * this is the same as the marker color.
  1122. *
  1123. * @type {Highcharts.ColorString}
  1124. * @since 6.2.0
  1125. * @apioption connectors.marker.lineColor
  1126. */
  1127. /**
  1128. * Enable markers for the connectors.
  1129. */
  1130. enabled: false,
  1131. /**
  1132. * Horizontal alignment of the markers relative to the points.
  1133. *
  1134. * @type {Highcharts.AlignType}
  1135. */
  1136. align: 'center',
  1137. /**
  1138. * Vertical alignment of the markers relative to the points.
  1139. *
  1140. * @type {Highcharts.VerticalAlignType}
  1141. */
  1142. verticalAlign: 'middle',
  1143. /**
  1144. * Whether or not to draw the markers inside the points.
  1145. */
  1146. inside: false,
  1147. /**
  1148. * Set the line/border width of the pathfinder markers.
  1149. */
  1150. lineWidth: 1
  1151. },
  1152. /**
  1153. * Marker options specific to the start markers for this chart's
  1154. * Pathfinder connectors. Overrides the generic marker options.
  1155. *
  1156. * @extends connectors.marker
  1157. * @since 6.2.0
  1158. */
  1159. startMarker: {
  1160. /**
  1161. * Set the symbol of the connector start markers.
  1162. */
  1163. symbol: 'diamond'
  1164. },
  1165. /**
  1166. * Marker options specific to the end markers for this chart's
  1167. * Pathfinder connectors. Overrides the generic marker options.
  1168. *
  1169. * @extends connectors.marker
  1170. * @since 6.2.0
  1171. */
  1172. endMarker: {
  1173. /**
  1174. * Set the symbol of the connector end markers.
  1175. */
  1176. symbol: 'arrow-filled'
  1177. }
  1178. }
  1179. });
  1180. /**
  1181. * Override Pathfinder connector options for a series. Requires Highcharts Gantt
  1182. * to be loaded.
  1183. *
  1184. * @extends connectors
  1185. * @since 6.2.0
  1186. * @excluding enabled, algorithmMargin
  1187. * @product gantt
  1188. * @apioption plotOptions.series.connectors
  1189. */
  1190. /**
  1191. * Connect to a point. Requires Highcharts Gantt to be loaded. This option can
  1192. * be either a string, referring to the ID of another point, or an object, or an
  1193. * array of either. If the option is an array, each element defines a
  1194. * connection.
  1195. *
  1196. * @sample gantt/pathfinder/demo
  1197. * Different connection types
  1198. *
  1199. * @type {string|Array<string|*>|*}
  1200. * @extends plotOptions.series.connectors
  1201. * @since 6.2.0
  1202. * @excluding enabled
  1203. * @product gantt
  1204. * @apioption series.xrange.data.connect
  1205. */
  1206. /**
  1207. * The ID of the point to connect to.
  1208. *
  1209. * @type {string}
  1210. * @since 6.2.0
  1211. * @product gantt
  1212. * @apioption series.xrange.data.connect.to
  1213. */
  1214. /**
  1215. * Get point bounding box using plotX/plotY and shapeArgs. If using
  1216. * graphic.getBBox() directly, the bbox will be affected by animation.
  1217. *
  1218. * @private
  1219. * @function
  1220. *
  1221. * @param {Highcharts.Point} point
  1222. * The point to get BB of.
  1223. *
  1224. * @return {object}
  1225. * Result xMax, xMin, yMax, yMin.
  1226. */
  1227. function getPointBB(point) {
  1228. var shapeArgs = point.shapeArgs,
  1229. bb;
  1230. // Prefer using shapeArgs (columns)
  1231. if (shapeArgs) {
  1232. return {
  1233. xMin: shapeArgs.x,
  1234. xMax: shapeArgs.x + shapeArgs.width,
  1235. yMin: shapeArgs.y,
  1236. yMax: shapeArgs.y + shapeArgs.height
  1237. };
  1238. }
  1239. // Otherwise use plotX/plotY and bb
  1240. bb = point.graphic && point.graphic.getBBox();
  1241. return bb ? {
  1242. xMin: point.plotX - bb.width / 2,
  1243. xMax: point.plotX + bb.width / 2,
  1244. yMin: point.plotY - bb.height / 2,
  1245. yMax: point.plotY + bb.height / 2
  1246. } : null;
  1247. }
  1248. /**
  1249. * Calculate margin to place around obstacles for the pathfinder in pixels.
  1250. * Returns a minimum of 1 pixel margin.
  1251. *
  1252. * @private
  1253. * @function
  1254. *
  1255. * @param {Array<object>} obstacles
  1256. * Obstacles to calculate margin from.
  1257. *
  1258. * @return {number}
  1259. * The calculated margin in pixels. At least 1.
  1260. */
  1261. function calculateObstacleMargin(obstacles) {
  1262. var len = obstacles.length,
  1263. i = 0,
  1264. j,
  1265. obstacleDistance,
  1266. distances = [],
  1267. // Compute smallest distance between two rectangles
  1268. distance = function (a, b, bbMargin) {
  1269. // Count the distance even if we are slightly off
  1270. var margin = pick(bbMargin, 10),
  1271. yOverlap = a.yMax + margin > b.yMin - margin &&
  1272. a.yMin - margin < b.yMax + margin,
  1273. xOverlap = a.xMax + margin > b.xMin - margin &&
  1274. a.xMin - margin < b.xMax + margin,
  1275. xDistance = yOverlap ? (
  1276. a.xMin > b.xMax ? a.xMin - b.xMax : b.xMin - a.xMax
  1277. ) : Infinity,
  1278. yDistance = xOverlap ? (
  1279. a.yMin > b.yMax ? a.yMin - b.yMax : b.yMin - a.yMax
  1280. ) : Infinity;
  1281. // If the rectangles collide, try recomputing with smaller margin.
  1282. // If they collide anyway, discard the obstacle.
  1283. if (xOverlap && yOverlap) {
  1284. return (
  1285. margin ?
  1286. distance(a, b, Math.floor(margin / 2)) :
  1287. Infinity
  1288. );
  1289. }
  1290. return min(xDistance, yDistance);
  1291. };
  1292. // Go over all obstacles and compare them to the others.
  1293. for (; i < len; ++i) {
  1294. // Compare to all obstacles ahead. We will already have compared this
  1295. // obstacle to the ones before.
  1296. for (j = i + 1; j < len; ++j) {
  1297. obstacleDistance = distance(obstacles[i], obstacles[j]);
  1298. // TODO: Magic number 80
  1299. if (obstacleDistance < 80) { // Ignore large distances
  1300. distances.push(obstacleDistance);
  1301. }
  1302. }
  1303. }
  1304. // Ensure we always have at least one value, even in very spaceous charts
  1305. distances.push(80);
  1306. return max(
  1307. Math.floor(
  1308. distances.sort(function (a, b) {
  1309. return a - b;
  1310. })[
  1311. // Discard first 10% of the relevant distances, and then grab
  1312. // the smallest one.
  1313. Math.floor(distances.length / 10)
  1314. ] / 2 - 1 // Divide the distance by 2 and subtract 1.
  1315. ),
  1316. 1 // 1 is the minimum margin
  1317. );
  1318. }
  1319. /**
  1320. * The Connection class. Used internally to represent a connection between two
  1321. * points.
  1322. *
  1323. * @private
  1324. * @class
  1325. * @name Highcharts.Connection
  1326. *
  1327. * @param {Highcharts.Point} from
  1328. * Connection runs from this Point.
  1329. *
  1330. * @param {Highcharts.Point} to
  1331. * Connection runs to this Point.
  1332. *
  1333. * @param {Highcharts.ConnectorsOptions} [options]
  1334. * Connection options.
  1335. */
  1336. function Connection(from, to, options) {
  1337. this.init(from, to, options);
  1338. }
  1339. Connection.prototype = {
  1340. /**
  1341. * Initialize the Connection object. Used as constructor only.
  1342. *
  1343. * @function Highcharts.Connection#init
  1344. *
  1345. * @param {Highcharts.Point} from
  1346. * Connection runs from this Point.
  1347. *
  1348. * @param {Highcharts.Point} to
  1349. * Connection runs to this Point.
  1350. *
  1351. * @param {Highcharts.ConnectorsOptions} [options]
  1352. * Connection options.
  1353. */
  1354. init: function (from, to, options) {
  1355. this.fromPoint = from;
  1356. this.toPoint = to;
  1357. this.options = options;
  1358. this.chart = from.series.chart;
  1359. this.pathfinder = this.chart.pathfinder;
  1360. },
  1361. /**
  1362. * Add (or update) this connection's path on chart. Stores reference to the
  1363. * created element on this.graphics.path.
  1364. *
  1365. * @function Highcharts.Connection#renderPath
  1366. *
  1367. * @param {Highcharts.SVGPathArray} path
  1368. * Path to render, in array format. E.g. ['M', 0, 0, 'L', 10, 10]
  1369. *
  1370. * @param {Highcharts.SVGAttributes} [attribs]
  1371. * SVG attributes for the path.
  1372. *
  1373. * @param {Highcharts.AnimationOptionsObject} [animation]
  1374. * Animation options for the rendering.
  1375. *
  1376. * @param {Function} [complete]
  1377. * Callback function when the path has been rendered and animation is
  1378. * complete.
  1379. */
  1380. renderPath: function (path, attribs, animation) {
  1381. var connection = this,
  1382. chart = this.chart,
  1383. styledMode = chart.styledMode,
  1384. pathfinder = chart.pathfinder,
  1385. animate = !chart.options.chart.forExport && animation !== false,
  1386. pathGraphic = connection.graphics && connection.graphics.path,
  1387. anim;
  1388. // Add the SVG element of the pathfinder group if it doesn't exist
  1389. if (!pathfinder.group) {
  1390. pathfinder.group = chart.renderer.g()
  1391. .addClass('highcharts-pathfinder-group')
  1392. .attr({ zIndex: -1 })
  1393. .add(chart.seriesGroup);
  1394. }
  1395. // Shift the group to compensate for plot area.
  1396. // Note: Do this always (even when redrawing a path) to avoid issues
  1397. // when updating chart in a way that changes plot metrics.
  1398. pathfinder.group.translate(chart.plotLeft, chart.plotTop);
  1399. // Create path if does not exist
  1400. if (!(pathGraphic && pathGraphic.renderer)) {
  1401. pathGraphic = chart.renderer.path()
  1402. .add(pathfinder.group);
  1403. if (!styledMode) {
  1404. pathGraphic.attr({
  1405. opacity: 0
  1406. });
  1407. }
  1408. }
  1409. // Set path attribs and animate to the new path
  1410. pathGraphic.attr(attribs);
  1411. anim = { d: path };
  1412. if (!styledMode) {
  1413. anim.opacity = 1;
  1414. }
  1415. pathGraphic[animate ? 'animate' : 'attr'](anim, animation);
  1416. // Store reference on connection
  1417. this.graphics = this.graphics || {};
  1418. this.graphics.path = pathGraphic;
  1419. },
  1420. /**
  1421. * Calculate and add marker graphics for connection to the chart. The
  1422. * created/updated elements are stored on this.graphics.start and
  1423. * this.graphics.end.
  1424. *
  1425. * @function Highcharts.Connection#addMarker
  1426. *
  1427. * @param {string} type
  1428. * Marker type, either 'start' or 'end'.
  1429. *
  1430. * @param {Highcharts.ConnectorsMarkerOptions} options
  1431. * All options for this marker. Not calculated or merged with other
  1432. * options.
  1433. *
  1434. * @param {Highcharts.SVGPathArray} path
  1435. * Connection path in array format. This is used to calculate the
  1436. * rotation angle of the markers.
  1437. */
  1438. addMarker: function (type, options, path) {
  1439. var connection = this,
  1440. chart = connection.fromPoint.series.chart,
  1441. pathfinder = chart.pathfinder,
  1442. renderer = chart.renderer,
  1443. point = (
  1444. type === 'start' ?
  1445. connection.fromPoint :
  1446. connection.toPoint
  1447. ),
  1448. anchor = point.getPathfinderAnchorPoint(options),
  1449. markerVector,
  1450. radians,
  1451. rotation,
  1452. box,
  1453. width,
  1454. height,
  1455. pathVector;
  1456. if (!options.enabled) {
  1457. return;
  1458. }
  1459. // Last vector before start/end of path, used to get angle
  1460. if (type === 'start') {
  1461. pathVector = {
  1462. x: path[4],
  1463. y: path[5]
  1464. };
  1465. } else { // 'end'
  1466. pathVector = {
  1467. x: path[path.length - 5],
  1468. y: path[path.length - 4]
  1469. };
  1470. }
  1471. // Get angle between pathVector and anchor point and use it to create
  1472. // marker position.
  1473. radians = point.getRadiansToVector(pathVector, anchor);
  1474. markerVector = point.getMarkerVector(
  1475. radians,
  1476. options.radius,
  1477. anchor
  1478. );
  1479. // Rotation of marker is calculated from angle between pathVector and
  1480. // markerVector.
  1481. // (Note:
  1482. // Used to recalculate radians between markerVector and pathVector,
  1483. // but this should be the same as between pathVector and anchor.)
  1484. rotation = -radians / deg2rad;
  1485. if (options.width && options.height) {
  1486. width = options.width;
  1487. height = options.height;
  1488. } else {
  1489. width = height = options.radius * 2;
  1490. }
  1491. // Add graphics object if it does not exist
  1492. connection.graphics = connection.graphics || {};
  1493. box = {
  1494. x: markerVector.x - (width / 2),
  1495. y: markerVector.y - (height / 2),
  1496. width: width,
  1497. height: height,
  1498. rotation: rotation,
  1499. rotationOriginX: markerVector.x,
  1500. rotationOriginY: markerVector.y
  1501. };
  1502. if (!connection.graphics[type]) {
  1503. // Create new marker element
  1504. connection.graphics[type] = renderer.symbol(
  1505. options.symbol
  1506. )
  1507. .addClass(
  1508. 'highcharts-point-connecting-path-' + type + '-marker'
  1509. )
  1510. .attr(box)
  1511. .add(pathfinder.group);
  1512. if (!renderer.styledMode) {
  1513. connection.graphics[type].attr({
  1514. fill: options.color || connection.fromPoint.color,
  1515. stroke: options.lineColor,
  1516. 'stroke-width': options.lineWidth,
  1517. opacity: 0
  1518. })
  1519. .animate({
  1520. opacity: 1
  1521. }, point.series.options.animation);
  1522. }
  1523. } else {
  1524. connection.graphics[type].animate(box);
  1525. }
  1526. },
  1527. /**
  1528. * Calculate and return connection path.
  1529. * Note: Recalculates chart obstacles on demand if they aren't calculated.
  1530. *
  1531. * @function Highcharts.Connection#getPath
  1532. *
  1533. * @param {Highcharts.ConnectorsOptions} options
  1534. * Connector options. Not calculated or merged with other options.
  1535. *
  1536. * @return {Highcharts.SVHPathArray}
  1537. * Calculated SVG path data in array format.
  1538. */
  1539. getPath: function (options) {
  1540. var pathfinder = this.pathfinder,
  1541. chart = this.chart,
  1542. algorithm = pathfinder.algorithms[options.type],
  1543. chartObstacles = pathfinder.chartObstacles;
  1544. if (typeof algorithm !== 'function') {
  1545. H.error(
  1546. '"' + options.type + '" is not a Pathfinder algorithm.'
  1547. );
  1548. return;
  1549. }
  1550. // This function calculates obstacles on demand if they don't exist
  1551. if (algorithm.requiresObstacles && !chartObstacles) {
  1552. chartObstacles =
  1553. pathfinder.chartObstacles =
  1554. pathfinder.getChartObstacles(options);
  1555. // If the algorithmMargin was computed, store the result in default
  1556. // options.
  1557. chart.options.connectors.algorithmMargin = options.algorithmMargin;
  1558. // Cache some metrics too
  1559. pathfinder.chartObstacleMetrics =
  1560. pathfinder.getObstacleMetrics(chartObstacles);
  1561. }
  1562. // Get the SVG path
  1563. return algorithm(
  1564. // From
  1565. this.fromPoint.getPathfinderAnchorPoint(options.startMarker),
  1566. // To
  1567. this.toPoint.getPathfinderAnchorPoint(options.endMarker),
  1568. merge({
  1569. chartObstacles: chartObstacles,
  1570. lineObstacles: pathfinder.lineObstacles || [],
  1571. obstacleMetrics: pathfinder.chartObstacleMetrics,
  1572. hardBounds: {
  1573. xMin: 0,
  1574. xMax: chart.plotWidth,
  1575. yMin: 0,
  1576. yMax: chart.plotHeight
  1577. },
  1578. obstacleOptions: {
  1579. margin: options.algorithmMargin
  1580. },
  1581. startDirectionX: pathfinder.getAlgorithmStartDirection(
  1582. options.startMarker
  1583. )
  1584. }, options)
  1585. );
  1586. },
  1587. /**
  1588. * (re)Calculate and (re)draw the connection.
  1589. *
  1590. * @function Highcharts.Connection#render
  1591. */
  1592. render: function () {
  1593. var connection = this,
  1594. fromPoint = connection.fromPoint,
  1595. series = fromPoint.series,
  1596. chart = series.chart,
  1597. pathfinder = chart.pathfinder,
  1598. pathResult,
  1599. path,
  1600. options = merge(
  1601. chart.options.connectors, series.options.connectors,
  1602. fromPoint.options.connectors, connection.options
  1603. ),
  1604. attribs = {};
  1605. // Set path attribs
  1606. if (!chart.styledMode) {
  1607. attribs.stroke = options.lineColor || fromPoint.color;
  1608. attribs['stroke-width'] = options.lineWidth;
  1609. if (options.dashStyle) {
  1610. attribs.dashstyle = options.dashStyle;
  1611. }
  1612. }
  1613. attribs.class = 'highcharts-point-connecting-path ' +
  1614. 'highcharts-color-' + fromPoint.colorIndex;
  1615. options = merge(attribs, options);
  1616. // Set common marker options
  1617. if (!defined(options.marker.radius)) {
  1618. options.marker.radius = min(max(
  1619. Math.ceil((options.algorithmMargin || 8) / 2) - 1, 1
  1620. ), 5);
  1621. }
  1622. // Get the path
  1623. pathResult = connection.getPath(options);
  1624. path = pathResult.path;
  1625. // Always update obstacle storage with obstacles from this path.
  1626. // We don't know if future calls will need this for their algorithm.
  1627. if (pathResult.obstacles) {
  1628. pathfinder.lineObstacles = pathfinder.lineObstacles || [];
  1629. pathfinder.lineObstacles =
  1630. pathfinder.lineObstacles.concat(pathResult.obstacles);
  1631. }
  1632. // Add the calculated path to the pathfinder group
  1633. connection.renderPath(path, attribs, series.options.animation);
  1634. // Render the markers
  1635. connection.addMarker(
  1636. 'start',
  1637. merge(options.marker, options.startMarker),
  1638. path
  1639. );
  1640. connection.addMarker(
  1641. 'end',
  1642. merge(options.marker, options.endMarker),
  1643. path
  1644. );
  1645. },
  1646. /**
  1647. * Destroy connection by destroying the added graphics elements.
  1648. *
  1649. * @function Highcharts.Connection#destroy
  1650. */
  1651. destroy: function () {
  1652. if (this.graphics) {
  1653. H.objectEach(this.graphics, function (val) {
  1654. val.destroy();
  1655. });
  1656. delete this.graphics;
  1657. }
  1658. }
  1659. };
  1660. /**
  1661. * The Pathfinder class.
  1662. *
  1663. * @private
  1664. * @class
  1665. * @name Highcharts.Pathfinder
  1666. *
  1667. * @param {Highcharts.Chart} chart
  1668. * The chart to operate on.
  1669. */
  1670. function Pathfinder(chart) {
  1671. this.init(chart);
  1672. }
  1673. Pathfinder.prototype = {
  1674. /**
  1675. * @name Highcharts.Pathfinder#algorithms
  1676. * @type {Highcharts.Dictionary<Function>}
  1677. */
  1678. algorithms: pathfinderAlgorithms,
  1679. /**
  1680. * Initialize the Pathfinder object.
  1681. *
  1682. * @function Highcharts.Pathfinder#init
  1683. *
  1684. * @param {Highcharts.Chart} chart
  1685. * The chart context.
  1686. */
  1687. init: function (chart) {
  1688. // Initialize pathfinder with chart context
  1689. this.chart = chart;
  1690. // Init connection reference list
  1691. this.connections = [];
  1692. // Recalculate paths/obstacles on chart redraw
  1693. addEvent(chart, 'redraw', function () {
  1694. this.pathfinder.update();
  1695. });
  1696. },
  1697. /**
  1698. * Update Pathfinder connections from scratch.
  1699. *
  1700. * @function Highcharts.Pathfinder#update
  1701. *
  1702. * @param {boolean} deferRender
  1703. * Whether or not to defer rendering of connections until
  1704. * series.afterAnimate event has fired. Used on first render.
  1705. */
  1706. update: function (deferRender) {
  1707. var chart = this.chart,
  1708. pathfinder = this,
  1709. oldConnections = pathfinder.connections;
  1710. // Rebuild pathfinder connections from options
  1711. pathfinder.connections = [];
  1712. chart.series.forEach(function (series) {
  1713. if (series.visible) {
  1714. series.points.forEach(function (point) {
  1715. var to,
  1716. connects = (
  1717. point.options &&
  1718. point.options.connect &&
  1719. H.splat(point.options.connect)
  1720. );
  1721. if (point.visible && point.isInside !== false && connects) {
  1722. connects.forEach(function (connect) {
  1723. to = chart.get(
  1724. typeof connect === 'string' ?
  1725. connect : connect.to
  1726. );
  1727. if (
  1728. to instanceof H.Point &&
  1729. to.series.visible &&
  1730. to.visible &&
  1731. to.isInside !== false
  1732. ) {
  1733. // Add new connection
  1734. pathfinder.connections.push(new Connection(
  1735. point, // from
  1736. to,
  1737. typeof connect === 'string' ? {} : connect
  1738. ));
  1739. }
  1740. });
  1741. }
  1742. });
  1743. }
  1744. });
  1745. // Clear connections that should not be updated, and move old info over
  1746. // to new connections.
  1747. for (
  1748. var j = 0, k, found, lenOld = oldConnections.length,
  1749. lenNew = pathfinder.connections.length;
  1750. j < lenOld;
  1751. ++j
  1752. ) {
  1753. found = false;
  1754. for (k = 0; k < lenNew; ++k) {
  1755. if (
  1756. oldConnections[j].fromPoint ===
  1757. pathfinder.connections[k].fromPoint &&
  1758. oldConnections[j].toPoint ===
  1759. pathfinder.connections[k].toPoint
  1760. ) {
  1761. pathfinder.connections[k].graphics =
  1762. oldConnections[j].graphics;
  1763. found = true;
  1764. break;
  1765. }
  1766. }
  1767. if (!found) {
  1768. oldConnections[j].destroy();
  1769. }
  1770. }
  1771. // Clear obstacles to force recalculation. This must be done on every
  1772. // redraw in case positions have changed. Recalculation is handled in
  1773. // Connection.getPath on demand.
  1774. delete this.chartObstacles;
  1775. delete this.lineObstacles;
  1776. // Draw the pending connections
  1777. pathfinder.renderConnections(deferRender);
  1778. },
  1779. /**
  1780. * Draw the chart's connecting paths.
  1781. *
  1782. * @function Highcharts.Pathfinder#renderConnections
  1783. *
  1784. * @param {boolean} deferRender
  1785. * Whether or not to defer render until series animation is finished.
  1786. * Used on first render.
  1787. */
  1788. renderConnections: function (deferRender) {
  1789. if (deferRender) {
  1790. // Render after series are done animating
  1791. this.chart.series.forEach(function (series) {
  1792. var render = function () {
  1793. // Find pathfinder connections belonging to this series
  1794. // that haven't rendered, and render them now.
  1795. var pathfinder = series.chart.pathfinder,
  1796. conns = pathfinder && pathfinder.connections || [];
  1797. conns.forEach(function (connection) {
  1798. if (
  1799. connection.fromPoint &&
  1800. connection.fromPoint.series === series
  1801. ) {
  1802. connection.render();
  1803. }
  1804. });
  1805. if (series.pathfinderRemoveRenderEvent) {
  1806. series.pathfinderRemoveRenderEvent();
  1807. delete series.pathfinderRemoveRenderEvent;
  1808. }
  1809. };
  1810. if (series.options.animation === false) {
  1811. render();
  1812. } else {
  1813. series.pathfinderRemoveRenderEvent = addEvent(
  1814. series, 'afterAnimate', render
  1815. );
  1816. }
  1817. });
  1818. } else {
  1819. // Go through connections and render them
  1820. this.connections.forEach(function (connection) {
  1821. connection.render();
  1822. });
  1823. }
  1824. },
  1825. /**
  1826. * Get obstacles for the points in the chart. Does not include connecting
  1827. * lines from Pathfinder. Applies algorithmMargin to the obstacles.
  1828. *
  1829. * @function Highcharts.Pathfinder#getChartObstacles
  1830. *
  1831. * @param {object} options
  1832. * Options for the calculation. Currenlty only
  1833. * options.algorithmMargin.
  1834. *
  1835. * @return {Array<object>}
  1836. * An array of calculated obstacles. Each obstacle is defined as an
  1837. * object with xMin, xMax, yMin and yMax properties.
  1838. */
  1839. getChartObstacles: function (options) {
  1840. var obstacles = [],
  1841. series = this.chart.series,
  1842. margin = pick(options.algorithmMargin, 0),
  1843. calculatedMargin;
  1844. for (var i = 0, sLen = series.length; i < sLen; ++i) {
  1845. if (series[i].visible) {
  1846. for (
  1847. var j = 0, pLen = series[i].points.length, bb, point;
  1848. j < pLen;
  1849. ++j
  1850. ) {
  1851. point = series[i].points[j];
  1852. if (point.visible) {
  1853. bb = getPointBB(point);
  1854. if (bb) {
  1855. obstacles.push({
  1856. xMin: bb.xMin - margin,
  1857. xMax: bb.xMax + margin,
  1858. yMin: bb.yMin - margin,
  1859. yMax: bb.yMax + margin
  1860. });
  1861. }
  1862. }
  1863. }
  1864. }
  1865. }
  1866. // Sort obstacles by xMin for optimization
  1867. obstacles = obstacles.sort(function (a, b) {
  1868. return a.xMin - b.xMin;
  1869. });
  1870. // Add auto-calculated margin if the option is not defined
  1871. if (!defined(options.algorithmMargin)) {
  1872. calculatedMargin =
  1873. options.algorithmMargin =
  1874. calculateObstacleMargin(obstacles);
  1875. obstacles.forEach(function (obstacle) {
  1876. obstacle.xMin -= calculatedMargin;
  1877. obstacle.xMax += calculatedMargin;
  1878. obstacle.yMin -= calculatedMargin;
  1879. obstacle.yMax += calculatedMargin;
  1880. });
  1881. }
  1882. return obstacles;
  1883. },
  1884. /**
  1885. * Utility function to get metrics for obstacles:
  1886. * - Widest obstacle width
  1887. * - Tallest obstacle height
  1888. *
  1889. * @function Highcharts.Pathfinder#getObstacleMetrics
  1890. *
  1891. * @param {Array<object>} obstacles
  1892. * An array of obstacles to inspect.
  1893. *
  1894. * @return {object}
  1895. * The calculated metrics, as an object with maxHeight and maxWidth
  1896. * properties.
  1897. */
  1898. getObstacleMetrics: function (obstacles) {
  1899. var maxWidth = 0,
  1900. maxHeight = 0,
  1901. width,
  1902. height,
  1903. i = obstacles.length;
  1904. while (i--) {
  1905. width = obstacles[i].xMax - obstacles[i].xMin;
  1906. height = obstacles[i].yMax - obstacles[i].yMin;
  1907. if (maxWidth < width) {
  1908. maxWidth = width;
  1909. }
  1910. if (maxHeight < height) {
  1911. maxHeight = height;
  1912. }
  1913. }
  1914. return {
  1915. maxHeight: maxHeight,
  1916. maxWidth: maxWidth
  1917. };
  1918. },
  1919. /**
  1920. * Utility to get which direction to start the pathfinding algorithm
  1921. * (X vs Y), calculated from a set of marker options.
  1922. *
  1923. * @function Highcharts.Pathfinder#getAlgorithmStartDirection
  1924. *
  1925. * @param {Highcharts.ConnectorsMarkerOptions} markerOptions
  1926. * Marker options to calculate from.
  1927. *
  1928. * @return {boolean}
  1929. * Returns true for X, false for Y, and undefined for autocalculate.
  1930. */
  1931. getAlgorithmStartDirection: function (markerOptions) {
  1932. var xCenter = markerOptions.align !== 'left' &&
  1933. markerOptions.align !== 'right',
  1934. yCenter = markerOptions.verticalAlign !== 'top' &&
  1935. markerOptions.verticalAlign !== 'bottom',
  1936. undef;
  1937. return xCenter ?
  1938. (yCenter ? undef : false) : // x is centered
  1939. (yCenter ? true : undef); // x is off-center
  1940. }
  1941. };
  1942. // Add to Highcharts namespace
  1943. H.Connection = Connection;
  1944. H.Pathfinder = Pathfinder;
  1945. // Add pathfinding capabilities to Points
  1946. extend(H.Point.prototype, /** @lends Point.prototype */ {
  1947. /**
  1948. * Get coordinates of anchor point for pathfinder connection.
  1949. *
  1950. * @private
  1951. * @function Highcharts.Point#getPathfinderAnchorPoint
  1952. *
  1953. * @param {Highcharts.ConnectorsMarkerOptions} markerOptions
  1954. * Connection options for position on point.
  1955. *
  1956. * @return {object}
  1957. * An object with x/y properties for the position. Coordinates are
  1958. * in plot values, not relative to point.
  1959. */
  1960. getPathfinderAnchorPoint: function (markerOptions) {
  1961. var bb = getPointBB(this),
  1962. x,
  1963. y;
  1964. switch (markerOptions.align) { // eslint-disable-line default-case
  1965. case 'right':
  1966. x = 'xMax';
  1967. break;
  1968. case 'left':
  1969. x = 'xMin';
  1970. }
  1971. switch (markerOptions.verticalAlign) { // eslint-disable-line default-case
  1972. case 'top':
  1973. y = 'yMin';
  1974. break;
  1975. case 'bottom':
  1976. y = 'yMax';
  1977. }
  1978. return {
  1979. x: x ? bb[x] : (bb.xMin + bb.xMax) / 2,
  1980. y: y ? bb[y] : (bb.yMin + bb.yMax) / 2
  1981. };
  1982. },
  1983. /**
  1984. * Utility to get the angle from one point to another.
  1985. *
  1986. * @private
  1987. * @function Highcharts.Point#getRadiansToVector
  1988. *
  1989. * @param {object} v1
  1990. * The first vector, as an object with x/y properties.
  1991. *
  1992. * @param {object} v2
  1993. * The second vector, as an object with x/y properties.
  1994. *
  1995. * @return {number}
  1996. * The angle in degrees
  1997. */
  1998. getRadiansToVector: function (v1, v2) {
  1999. var box;
  2000. if (!defined(v2)) {
  2001. box = getPointBB(this);
  2002. v2 = {
  2003. x: (box.xMin + box.xMax) / 2,
  2004. y: (box.yMin + box.yMax) / 2
  2005. };
  2006. }
  2007. return Math.atan2(v2.y - v1.y, v1.x - v2.x);
  2008. },
  2009. /**
  2010. * Utility to get the position of the marker, based on the path angle and
  2011. * the marker's radius.
  2012. *
  2013. * @private
  2014. * @function Highcharts.Point#getMarkerVector
  2015. *
  2016. * @param {number} radians
  2017. * The angle in radians from the point center to another vector.
  2018. *
  2019. * @param {number} markerRadius
  2020. * The radius of the marker, to calculate the additional distance to
  2021. * the center of the marker.
  2022. *
  2023. * @param {object} anchor
  2024. * The anchor point of the path and marker as an object with x/y
  2025. * properties.
  2026. *
  2027. * @return {object}
  2028. * The marker vector as an object with x/y properties.
  2029. */
  2030. getMarkerVector: function (radians, markerRadius, anchor) {
  2031. var twoPI = Math.PI * 2.0,
  2032. theta = radians,
  2033. bb = getPointBB(this),
  2034. rectWidth = bb.xMax - bb.xMin,
  2035. rectHeight = bb.yMax - bb.yMin,
  2036. rAtan = Math.atan2(rectHeight, rectWidth),
  2037. tanTheta = 1,
  2038. leftOrRightRegion = false,
  2039. rectHalfWidth = rectWidth / 2.0,
  2040. rectHalfHeight = rectHeight / 2.0,
  2041. rectHorizontalCenter = bb.xMin + rectHalfWidth,
  2042. rectVerticalCenter = bb.yMin + rectHalfHeight,
  2043. edgePoint = {
  2044. x: rectHorizontalCenter,
  2045. y: rectVerticalCenter
  2046. },
  2047. markerPoint = {},
  2048. xFactor = 1,
  2049. yFactor = 1;
  2050. while (theta < -Math.PI) {
  2051. theta += twoPI;
  2052. }
  2053. while (theta > Math.PI) {
  2054. theta -= twoPI;
  2055. }
  2056. tanTheta = Math.tan(theta);
  2057. if ((theta > -rAtan) && (theta <= rAtan)) {
  2058. // Right side
  2059. yFactor = -1;
  2060. leftOrRightRegion = true;
  2061. } else if (theta > rAtan && theta <= (Math.PI - rAtan)) {
  2062. // Top side
  2063. yFactor = -1;
  2064. } else if (theta > (Math.PI - rAtan) || theta <= -(Math.PI - rAtan)) {
  2065. // Left side
  2066. xFactor = -1;
  2067. leftOrRightRegion = true;
  2068. } else {
  2069. // Bottom side
  2070. xFactor = -1;
  2071. }
  2072. // Correct the edgePoint according to the placement of the marker
  2073. if (leftOrRightRegion) {
  2074. edgePoint.x += xFactor * (rectHalfWidth);
  2075. edgePoint.y += yFactor * (rectHalfWidth) * tanTheta;
  2076. } else {
  2077. edgePoint.x += xFactor * (rectHeight / (2.0 * tanTheta));
  2078. edgePoint.y += yFactor * (rectHalfHeight);
  2079. }
  2080. if (anchor.x !== rectHorizontalCenter) {
  2081. edgePoint.x = anchor.x;
  2082. }
  2083. if (anchor.y !== rectVerticalCenter) {
  2084. edgePoint.y = anchor.y;
  2085. }
  2086. markerPoint.x = edgePoint.x + (markerRadius * Math.cos(theta));
  2087. markerPoint.y = edgePoint.y - (markerRadius * Math.sin(theta));
  2088. return markerPoint;
  2089. }
  2090. });
  2091. // Warn if using legacy options. Copy the options over. Note that this will
  2092. // still break if using the legacy options in chart.update, addSeries etc.
  2093. function warnLegacy(chart) {
  2094. if (
  2095. chart.options.pathfinder ||
  2096. chart.series.reduce(function (acc, series) {
  2097. if (series.options) {
  2098. merge(
  2099. true,
  2100. (
  2101. series.options.connectors = series.options.connectors ||
  2102. {}
  2103. ), series.options.pathfinder
  2104. );
  2105. }
  2106. return acc || series.options && series.options.pathfinder;
  2107. }, false)
  2108. ) {
  2109. merge(
  2110. true,
  2111. (chart.options.connectors = chart.options.connectors || {}),
  2112. chart.options.pathfinder
  2113. );
  2114. H.error('WARNING: Pathfinder options have been renamed. ' +
  2115. 'Use "chart.connectors" or "series.connectors" instead.');
  2116. }
  2117. }
  2118. // Initialize Pathfinder for charts
  2119. H.Chart.prototype.callbacks.push(function (chart) {
  2120. var options = chart.options;
  2121. if (options.connectors.enabled !== false) {
  2122. warnLegacy(chart);
  2123. this.pathfinder = new Pathfinder(this);
  2124. this.pathfinder.update(true); // First draw, defer render
  2125. }
  2126. });
  2127. }(Highcharts, algorithms));
  2128. return (function () {
  2129. }());
  2130. }));