Pathfinder.js 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298
  1. /* *
  2. * (c) 2016 Highsoft AS
  3. * Authors: Øystein Moseng, Lars A. V. Cabrera
  4. *
  5. * License: www.highcharts.com/license
  6. */
  7. 'use strict';
  8. import H from '../parts/Globals.js';
  9. import '../parts/Point.js';
  10. import '../parts/Utilities.js';
  11. import pathfinderAlgorithms from 'PathfinderAlgorithms.js';
  12. import 'ArrowSymbols.js';
  13. var defined = H.defined,
  14. deg2rad = H.deg2rad,
  15. extend = H.extend,
  16. addEvent = H.addEvent,
  17. merge = H.merge,
  18. pick = H.pick,
  19. max = Math.max,
  20. min = Math.min;
  21. /*
  22. @todo:
  23. - Document how to write your own algorithms
  24. - Consider adding a Point.pathTo method that wraps creating a connection
  25. and rendering it
  26. */
  27. // Set default Pathfinder options
  28. extend(H.defaultOptions, {
  29. /**
  30. * The Pathfinder module allows you to define connections between any two
  31. * points, represented as lines - optionally with markers for the start
  32. * and/or end points. Multiple algorithms are available for calculating how
  33. * the connecting lines are drawn.
  34. *
  35. * Connector functionality requires Highcharts Gantt to be loaded. In Gantt
  36. * charts, the connectors are used to draw dependencies between tasks.
  37. *
  38. * @see [dependency](series.gantt.data.dependency)
  39. *
  40. * @sample gantt/pathfinder/demo
  41. * Pathfinder connections
  42. *
  43. * @product gantt
  44. * @optionparent connectors
  45. */
  46. connectors: {
  47. /**
  48. * Enable connectors for this chart. Requires Highcharts Gantt.
  49. *
  50. * @type {boolean}
  51. * @default true
  52. * @since 6.2.0
  53. * @apioption connectors.enabled
  54. */
  55. /**
  56. * Set the default dash style for this chart's connecting lines.
  57. *
  58. * @type {string}
  59. * @default solid
  60. * @since 6.2.0
  61. * @apioption connectors.dashStyle
  62. */
  63. /**
  64. * Set the default color for this chart's Pathfinder connecting lines.
  65. * Defaults to the color of the point being connected.
  66. *
  67. * @type {Highcharts.ColorString}
  68. * @since 6.2.0
  69. * @apioption connectors.lineColor
  70. */
  71. /**
  72. * Set the default pathfinder margin to use, in pixels. Some Pathfinder
  73. * algorithms attempt to avoid obstacles, such as other points in the
  74. * chart. These algorithms use this margin to determine how close lines
  75. * can be to an obstacle. The default is to compute this automatically
  76. * from the size of the obstacles in the chart.
  77. *
  78. * To draw connecting lines close to existing points, set this to a low
  79. * number. For more space around existing points, set this number
  80. * higher.
  81. *
  82. * @sample gantt/pathfinder/algorithm-margin
  83. * Small algorithmMargin
  84. *
  85. * @type {number}
  86. * @since 6.2.0
  87. * @apioption connectors.algorithmMargin
  88. */
  89. /**
  90. * Set the default pathfinder algorithm to use for this chart. It is
  91. * possible to define your own algorithms by adding them to the
  92. * Highcharts.Pathfinder.prototype.algorithms object before the chart
  93. * has been created.
  94. *
  95. * The default algorithms are as follows:
  96. *
  97. * `straight`: Draws a straight line between the connecting
  98. * points. Does not avoid other points when drawing.
  99. *
  100. * `simpleConnect`: Finds a path between the points using right angles
  101. * only. Takes only starting/ending points into
  102. * account, and will not avoid other points.
  103. *
  104. * `fastAvoid`: Finds a path between the points using right angles
  105. * only. Will attempt to avoid other points, but its
  106. * focus is performance over accuracy. Works well with
  107. * less dense datasets.
  108. *
  109. * Default value: `straight` is used as default for most series types,
  110. * while `simpleConnect` is used as default for Gantt series, to show
  111. * dependencies between points.
  112. *
  113. * @sample gantt/pathfinder/demo
  114. * Different types used
  115. *
  116. * @default undefined
  117. * @since 6.2.0
  118. * @validvalue ["straight", "simpleConnect", "fastAvoid"]
  119. */
  120. type: 'straight',
  121. /**
  122. * Set the default pixel width for this chart's Pathfinder connecting
  123. * lines.
  124. *
  125. * @since 6.2.0
  126. */
  127. lineWidth: 1,
  128. /**
  129. * Marker options for this chart's Pathfinder connectors. Note that
  130. * this option is overridden by the `startMarker` and `endMarker`
  131. * options.
  132. *
  133. * @since 6.2.0
  134. */
  135. marker: {
  136. /**
  137. * Set the radius of the connector markers. The default is
  138. * automatically computed based on the algorithmMargin setting.
  139. *
  140. * Setting marker.width and marker.height will override this
  141. * setting.
  142. *
  143. * @type {number}
  144. * @since 6.2.0
  145. * @apioption connectors.marker.radius
  146. */
  147. /**
  148. * Set the width of the connector markers. If not supplied, this
  149. * is inferred from the marker radius.
  150. *
  151. * @type {number}
  152. * @since 6.2.0
  153. * @apioption connectors.marker.width
  154. */
  155. /**
  156. * Set the height of the connector markers. If not supplied, this
  157. * is inferred from the marker radius.
  158. *
  159. * @type {number}
  160. * @since 6.2.0
  161. * @apioption connectors.marker.height
  162. */
  163. /**
  164. * Set the color of the connector markers. By default this is the
  165. * same as the connector color.
  166. *
  167. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  168. * @since 6.2.0
  169. * @apioption connectors.marker.color
  170. */
  171. /**
  172. * Set the line/border color of the connector markers. By default
  173. * this is the same as the marker color.
  174. *
  175. * @type {Highcharts.ColorString}
  176. * @since 6.2.0
  177. * @apioption connectors.marker.lineColor
  178. */
  179. /**
  180. * Enable markers for the connectors.
  181. */
  182. enabled: false,
  183. /**
  184. * Horizontal alignment of the markers relative to the points.
  185. *
  186. * @type {Highcharts.AlignType}
  187. */
  188. align: 'center',
  189. /**
  190. * Vertical alignment of the markers relative to the points.
  191. *
  192. * @type {Highcharts.VerticalAlignType}
  193. */
  194. verticalAlign: 'middle',
  195. /**
  196. * Whether or not to draw the markers inside the points.
  197. */
  198. inside: false,
  199. /**
  200. * Set the line/border width of the pathfinder markers.
  201. */
  202. lineWidth: 1
  203. },
  204. /**
  205. * Marker options specific to the start markers for this chart's
  206. * Pathfinder connectors. Overrides the generic marker options.
  207. *
  208. * @extends connectors.marker
  209. * @since 6.2.0
  210. */
  211. startMarker: {
  212. /**
  213. * Set the symbol of the connector start markers.
  214. */
  215. symbol: 'diamond'
  216. },
  217. /**
  218. * Marker options specific to the end markers for this chart's
  219. * Pathfinder connectors. Overrides the generic marker options.
  220. *
  221. * @extends connectors.marker
  222. * @since 6.2.0
  223. */
  224. endMarker: {
  225. /**
  226. * Set the symbol of the connector end markers.
  227. */
  228. symbol: 'arrow-filled'
  229. }
  230. }
  231. });
  232. /**
  233. * Override Pathfinder connector options for a series. Requires Highcharts Gantt
  234. * to be loaded.
  235. *
  236. * @extends connectors
  237. * @since 6.2.0
  238. * @excluding enabled, algorithmMargin
  239. * @product gantt
  240. * @apioption plotOptions.series.connectors
  241. */
  242. /**
  243. * Connect to a point. Requires Highcharts Gantt to be loaded. This option can
  244. * be either a string, referring to the ID of another point, or an object, or an
  245. * array of either. If the option is an array, each element defines a
  246. * connection.
  247. *
  248. * @sample gantt/pathfinder/demo
  249. * Different connection types
  250. *
  251. * @type {string|Array<string|*>|*}
  252. * @extends plotOptions.series.connectors
  253. * @since 6.2.0
  254. * @excluding enabled
  255. * @product gantt
  256. * @apioption series.xrange.data.connect
  257. */
  258. /**
  259. * The ID of the point to connect to.
  260. *
  261. * @type {string}
  262. * @since 6.2.0
  263. * @product gantt
  264. * @apioption series.xrange.data.connect.to
  265. */
  266. /**
  267. * Get point bounding box using plotX/plotY and shapeArgs. If using
  268. * graphic.getBBox() directly, the bbox will be affected by animation.
  269. *
  270. * @private
  271. * @function
  272. *
  273. * @param {Highcharts.Point} point
  274. * The point to get BB of.
  275. *
  276. * @return {object}
  277. * Result xMax, xMin, yMax, yMin.
  278. */
  279. function getPointBB(point) {
  280. var shapeArgs = point.shapeArgs,
  281. bb;
  282. // Prefer using shapeArgs (columns)
  283. if (shapeArgs) {
  284. return {
  285. xMin: shapeArgs.x,
  286. xMax: shapeArgs.x + shapeArgs.width,
  287. yMin: shapeArgs.y,
  288. yMax: shapeArgs.y + shapeArgs.height
  289. };
  290. }
  291. // Otherwise use plotX/plotY and bb
  292. bb = point.graphic && point.graphic.getBBox();
  293. return bb ? {
  294. xMin: point.plotX - bb.width / 2,
  295. xMax: point.plotX + bb.width / 2,
  296. yMin: point.plotY - bb.height / 2,
  297. yMax: point.plotY + bb.height / 2
  298. } : null;
  299. }
  300. /**
  301. * Calculate margin to place around obstacles for the pathfinder in pixels.
  302. * Returns a minimum of 1 pixel margin.
  303. *
  304. * @private
  305. * @function
  306. *
  307. * @param {Array<object>} obstacles
  308. * Obstacles to calculate margin from.
  309. *
  310. * @return {number}
  311. * The calculated margin in pixels. At least 1.
  312. */
  313. function calculateObstacleMargin(obstacles) {
  314. var len = obstacles.length,
  315. i = 0,
  316. j,
  317. obstacleDistance,
  318. distances = [],
  319. // Compute smallest distance between two rectangles
  320. distance = function (a, b, bbMargin) {
  321. // Count the distance even if we are slightly off
  322. var margin = pick(bbMargin, 10),
  323. yOverlap = a.yMax + margin > b.yMin - margin &&
  324. a.yMin - margin < b.yMax + margin,
  325. xOverlap = a.xMax + margin > b.xMin - margin &&
  326. a.xMin - margin < b.xMax + margin,
  327. xDistance = yOverlap ? (
  328. a.xMin > b.xMax ? a.xMin - b.xMax : b.xMin - a.xMax
  329. ) : Infinity,
  330. yDistance = xOverlap ? (
  331. a.yMin > b.yMax ? a.yMin - b.yMax : b.yMin - a.yMax
  332. ) : Infinity;
  333. // If the rectangles collide, try recomputing with smaller margin.
  334. // If they collide anyway, discard the obstacle.
  335. if (xOverlap && yOverlap) {
  336. return (
  337. margin ?
  338. distance(a, b, Math.floor(margin / 2)) :
  339. Infinity
  340. );
  341. }
  342. return min(xDistance, yDistance);
  343. };
  344. // Go over all obstacles and compare them to the others.
  345. for (; i < len; ++i) {
  346. // Compare to all obstacles ahead. We will already have compared this
  347. // obstacle to the ones before.
  348. for (j = i + 1; j < len; ++j) {
  349. obstacleDistance = distance(obstacles[i], obstacles[j]);
  350. // TODO: Magic number 80
  351. if (obstacleDistance < 80) { // Ignore large distances
  352. distances.push(obstacleDistance);
  353. }
  354. }
  355. }
  356. // Ensure we always have at least one value, even in very spaceous charts
  357. distances.push(80);
  358. return max(
  359. Math.floor(
  360. distances.sort(function (a, b) {
  361. return a - b;
  362. })[
  363. // Discard first 10% of the relevant distances, and then grab
  364. // the smallest one.
  365. Math.floor(distances.length / 10)
  366. ] / 2 - 1 // Divide the distance by 2 and subtract 1.
  367. ),
  368. 1 // 1 is the minimum margin
  369. );
  370. }
  371. /**
  372. * The Connection class. Used internally to represent a connection between two
  373. * points.
  374. *
  375. * @private
  376. * @class
  377. * @name Highcharts.Connection
  378. *
  379. * @param {Highcharts.Point} from
  380. * Connection runs from this Point.
  381. *
  382. * @param {Highcharts.Point} to
  383. * Connection runs to this Point.
  384. *
  385. * @param {Highcharts.ConnectorsOptions} [options]
  386. * Connection options.
  387. */
  388. function Connection(from, to, options) {
  389. this.init(from, to, options);
  390. }
  391. Connection.prototype = {
  392. /**
  393. * Initialize the Connection object. Used as constructor only.
  394. *
  395. * @function Highcharts.Connection#init
  396. *
  397. * @param {Highcharts.Point} from
  398. * Connection runs from this Point.
  399. *
  400. * @param {Highcharts.Point} to
  401. * Connection runs to this Point.
  402. *
  403. * @param {Highcharts.ConnectorsOptions} [options]
  404. * Connection options.
  405. */
  406. init: function (from, to, options) {
  407. this.fromPoint = from;
  408. this.toPoint = to;
  409. this.options = options;
  410. this.chart = from.series.chart;
  411. this.pathfinder = this.chart.pathfinder;
  412. },
  413. /**
  414. * Add (or update) this connection's path on chart. Stores reference to the
  415. * created element on this.graphics.path.
  416. *
  417. * @function Highcharts.Connection#renderPath
  418. *
  419. * @param {Highcharts.SVGPathArray} path
  420. * Path to render, in array format. E.g. ['M', 0, 0, 'L', 10, 10]
  421. *
  422. * @param {Highcharts.SVGAttributes} [attribs]
  423. * SVG attributes for the path.
  424. *
  425. * @param {Highcharts.AnimationOptionsObject} [animation]
  426. * Animation options for the rendering.
  427. *
  428. * @param {Function} [complete]
  429. * Callback function when the path has been rendered and animation is
  430. * complete.
  431. */
  432. renderPath: function (path, attribs, animation) {
  433. var connection = this,
  434. chart = this.chart,
  435. styledMode = chart.styledMode,
  436. pathfinder = chart.pathfinder,
  437. animate = !chart.options.chart.forExport && animation !== false,
  438. pathGraphic = connection.graphics && connection.graphics.path,
  439. anim;
  440. // Add the SVG element of the pathfinder group if it doesn't exist
  441. if (!pathfinder.group) {
  442. pathfinder.group = chart.renderer.g()
  443. .addClass('highcharts-pathfinder-group')
  444. .attr({ zIndex: -1 })
  445. .add(chart.seriesGroup);
  446. }
  447. // Shift the group to compensate for plot area.
  448. // Note: Do this always (even when redrawing a path) to avoid issues
  449. // when updating chart in a way that changes plot metrics.
  450. pathfinder.group.translate(chart.plotLeft, chart.plotTop);
  451. // Create path if does not exist
  452. if (!(pathGraphic && pathGraphic.renderer)) {
  453. pathGraphic = chart.renderer.path()
  454. .add(pathfinder.group);
  455. if (!styledMode) {
  456. pathGraphic.attr({
  457. opacity: 0
  458. });
  459. }
  460. }
  461. // Set path attribs and animate to the new path
  462. pathGraphic.attr(attribs);
  463. anim = { d: path };
  464. if (!styledMode) {
  465. anim.opacity = 1;
  466. }
  467. pathGraphic[animate ? 'animate' : 'attr'](anim, animation);
  468. // Store reference on connection
  469. this.graphics = this.graphics || {};
  470. this.graphics.path = pathGraphic;
  471. },
  472. /**
  473. * Calculate and add marker graphics for connection to the chart. The
  474. * created/updated elements are stored on this.graphics.start and
  475. * this.graphics.end.
  476. *
  477. * @function Highcharts.Connection#addMarker
  478. *
  479. * @param {string} type
  480. * Marker type, either 'start' or 'end'.
  481. *
  482. * @param {Highcharts.ConnectorsMarkerOptions} options
  483. * All options for this marker. Not calculated or merged with other
  484. * options.
  485. *
  486. * @param {Highcharts.SVGPathArray} path
  487. * Connection path in array format. This is used to calculate the
  488. * rotation angle of the markers.
  489. */
  490. addMarker: function (type, options, path) {
  491. var connection = this,
  492. chart = connection.fromPoint.series.chart,
  493. pathfinder = chart.pathfinder,
  494. renderer = chart.renderer,
  495. point = (
  496. type === 'start' ?
  497. connection.fromPoint :
  498. connection.toPoint
  499. ),
  500. anchor = point.getPathfinderAnchorPoint(options),
  501. markerVector,
  502. radians,
  503. rotation,
  504. box,
  505. width,
  506. height,
  507. pathVector;
  508. if (!options.enabled) {
  509. return;
  510. }
  511. // Last vector before start/end of path, used to get angle
  512. if (type === 'start') {
  513. pathVector = {
  514. x: path[4],
  515. y: path[5]
  516. };
  517. } else { // 'end'
  518. pathVector = {
  519. x: path[path.length - 5],
  520. y: path[path.length - 4]
  521. };
  522. }
  523. // Get angle between pathVector and anchor point and use it to create
  524. // marker position.
  525. radians = point.getRadiansToVector(pathVector, anchor);
  526. markerVector = point.getMarkerVector(
  527. radians,
  528. options.radius,
  529. anchor
  530. );
  531. // Rotation of marker is calculated from angle between pathVector and
  532. // markerVector.
  533. // (Note:
  534. // Used to recalculate radians between markerVector and pathVector,
  535. // but this should be the same as between pathVector and anchor.)
  536. rotation = -radians / deg2rad;
  537. if (options.width && options.height) {
  538. width = options.width;
  539. height = options.height;
  540. } else {
  541. width = height = options.radius * 2;
  542. }
  543. // Add graphics object if it does not exist
  544. connection.graphics = connection.graphics || {};
  545. box = {
  546. x: markerVector.x - (width / 2),
  547. y: markerVector.y - (height / 2),
  548. width: width,
  549. height: height,
  550. rotation: rotation,
  551. rotationOriginX: markerVector.x,
  552. rotationOriginY: markerVector.y
  553. };
  554. if (!connection.graphics[type]) {
  555. // Create new marker element
  556. connection.graphics[type] = renderer.symbol(
  557. options.symbol
  558. )
  559. .addClass(
  560. 'highcharts-point-connecting-path-' + type + '-marker'
  561. )
  562. .attr(box)
  563. .add(pathfinder.group);
  564. if (!renderer.styledMode) {
  565. connection.graphics[type].attr({
  566. fill: options.color || connection.fromPoint.color,
  567. stroke: options.lineColor,
  568. 'stroke-width': options.lineWidth,
  569. opacity: 0
  570. })
  571. .animate({
  572. opacity: 1
  573. }, point.series.options.animation);
  574. }
  575. } else {
  576. connection.graphics[type].animate(box);
  577. }
  578. },
  579. /**
  580. * Calculate and return connection path.
  581. * Note: Recalculates chart obstacles on demand if they aren't calculated.
  582. *
  583. * @function Highcharts.Connection#getPath
  584. *
  585. * @param {Highcharts.ConnectorsOptions} options
  586. * Connector options. Not calculated or merged with other options.
  587. *
  588. * @return {Highcharts.SVHPathArray}
  589. * Calculated SVG path data in array format.
  590. */
  591. getPath: function (options) {
  592. var pathfinder = this.pathfinder,
  593. chart = this.chart,
  594. algorithm = pathfinder.algorithms[options.type],
  595. chartObstacles = pathfinder.chartObstacles;
  596. if (typeof algorithm !== 'function') {
  597. H.error(
  598. '"' + options.type + '" is not a Pathfinder algorithm.'
  599. );
  600. return;
  601. }
  602. // This function calculates obstacles on demand if they don't exist
  603. if (algorithm.requiresObstacles && !chartObstacles) {
  604. chartObstacles =
  605. pathfinder.chartObstacles =
  606. pathfinder.getChartObstacles(options);
  607. // If the algorithmMargin was computed, store the result in default
  608. // options.
  609. chart.options.connectors.algorithmMargin = options.algorithmMargin;
  610. // Cache some metrics too
  611. pathfinder.chartObstacleMetrics =
  612. pathfinder.getObstacleMetrics(chartObstacles);
  613. }
  614. // Get the SVG path
  615. return algorithm(
  616. // From
  617. this.fromPoint.getPathfinderAnchorPoint(options.startMarker),
  618. // To
  619. this.toPoint.getPathfinderAnchorPoint(options.endMarker),
  620. merge({
  621. chartObstacles: chartObstacles,
  622. lineObstacles: pathfinder.lineObstacles || [],
  623. obstacleMetrics: pathfinder.chartObstacleMetrics,
  624. hardBounds: {
  625. xMin: 0,
  626. xMax: chart.plotWidth,
  627. yMin: 0,
  628. yMax: chart.plotHeight
  629. },
  630. obstacleOptions: {
  631. margin: options.algorithmMargin
  632. },
  633. startDirectionX: pathfinder.getAlgorithmStartDirection(
  634. options.startMarker
  635. )
  636. }, options)
  637. );
  638. },
  639. /**
  640. * (re)Calculate and (re)draw the connection.
  641. *
  642. * @function Highcharts.Connection#render
  643. */
  644. render: function () {
  645. var connection = this,
  646. fromPoint = connection.fromPoint,
  647. series = fromPoint.series,
  648. chart = series.chart,
  649. pathfinder = chart.pathfinder,
  650. pathResult,
  651. path,
  652. options = merge(
  653. chart.options.connectors, series.options.connectors,
  654. fromPoint.options.connectors, connection.options
  655. ),
  656. attribs = {};
  657. // Set path attribs
  658. if (!chart.styledMode) {
  659. attribs.stroke = options.lineColor || fromPoint.color;
  660. attribs['stroke-width'] = options.lineWidth;
  661. if (options.dashStyle) {
  662. attribs.dashstyle = options.dashStyle;
  663. }
  664. }
  665. attribs.class = 'highcharts-point-connecting-path ' +
  666. 'highcharts-color-' + fromPoint.colorIndex;
  667. options = merge(attribs, options);
  668. // Set common marker options
  669. if (!defined(options.marker.radius)) {
  670. options.marker.radius = min(max(
  671. Math.ceil((options.algorithmMargin || 8) / 2) - 1, 1
  672. ), 5);
  673. }
  674. // Get the path
  675. pathResult = connection.getPath(options);
  676. path = pathResult.path;
  677. // Always update obstacle storage with obstacles from this path.
  678. // We don't know if future calls will need this for their algorithm.
  679. if (pathResult.obstacles) {
  680. pathfinder.lineObstacles = pathfinder.lineObstacles || [];
  681. pathfinder.lineObstacles =
  682. pathfinder.lineObstacles.concat(pathResult.obstacles);
  683. }
  684. // Add the calculated path to the pathfinder group
  685. connection.renderPath(path, attribs, series.options.animation);
  686. // Render the markers
  687. connection.addMarker(
  688. 'start',
  689. merge(options.marker, options.startMarker),
  690. path
  691. );
  692. connection.addMarker(
  693. 'end',
  694. merge(options.marker, options.endMarker),
  695. path
  696. );
  697. },
  698. /**
  699. * Destroy connection by destroying the added graphics elements.
  700. *
  701. * @function Highcharts.Connection#destroy
  702. */
  703. destroy: function () {
  704. if (this.graphics) {
  705. H.objectEach(this.graphics, function (val) {
  706. val.destroy();
  707. });
  708. delete this.graphics;
  709. }
  710. }
  711. };
  712. /**
  713. * The Pathfinder class.
  714. *
  715. * @private
  716. * @class
  717. * @name Highcharts.Pathfinder
  718. *
  719. * @param {Highcharts.Chart} chart
  720. * The chart to operate on.
  721. */
  722. function Pathfinder(chart) {
  723. this.init(chart);
  724. }
  725. Pathfinder.prototype = {
  726. /**
  727. * @name Highcharts.Pathfinder#algorithms
  728. * @type {Highcharts.Dictionary<Function>}
  729. */
  730. algorithms: pathfinderAlgorithms,
  731. /**
  732. * Initialize the Pathfinder object.
  733. *
  734. * @function Highcharts.Pathfinder#init
  735. *
  736. * @param {Highcharts.Chart} chart
  737. * The chart context.
  738. */
  739. init: function (chart) {
  740. // Initialize pathfinder with chart context
  741. this.chart = chart;
  742. // Init connection reference list
  743. this.connections = [];
  744. // Recalculate paths/obstacles on chart redraw
  745. addEvent(chart, 'redraw', function () {
  746. this.pathfinder.update();
  747. });
  748. },
  749. /**
  750. * Update Pathfinder connections from scratch.
  751. *
  752. * @function Highcharts.Pathfinder#update
  753. *
  754. * @param {boolean} deferRender
  755. * Whether or not to defer rendering of connections until
  756. * series.afterAnimate event has fired. Used on first render.
  757. */
  758. update: function (deferRender) {
  759. var chart = this.chart,
  760. pathfinder = this,
  761. oldConnections = pathfinder.connections;
  762. // Rebuild pathfinder connections from options
  763. pathfinder.connections = [];
  764. chart.series.forEach(function (series) {
  765. if (series.visible) {
  766. series.points.forEach(function (point) {
  767. var to,
  768. connects = (
  769. point.options &&
  770. point.options.connect &&
  771. H.splat(point.options.connect)
  772. );
  773. if (point.visible && point.isInside !== false && connects) {
  774. connects.forEach(function (connect) {
  775. to = chart.get(
  776. typeof connect === 'string' ?
  777. connect : connect.to
  778. );
  779. if (
  780. to instanceof H.Point &&
  781. to.series.visible &&
  782. to.visible &&
  783. to.isInside !== false
  784. ) {
  785. // Add new connection
  786. pathfinder.connections.push(new Connection(
  787. point, // from
  788. to,
  789. typeof connect === 'string' ? {} : connect
  790. ));
  791. }
  792. });
  793. }
  794. });
  795. }
  796. });
  797. // Clear connections that should not be updated, and move old info over
  798. // to new connections.
  799. for (
  800. var j = 0, k, found, lenOld = oldConnections.length,
  801. lenNew = pathfinder.connections.length;
  802. j < lenOld;
  803. ++j
  804. ) {
  805. found = false;
  806. for (k = 0; k < lenNew; ++k) {
  807. if (
  808. oldConnections[j].fromPoint ===
  809. pathfinder.connections[k].fromPoint &&
  810. oldConnections[j].toPoint ===
  811. pathfinder.connections[k].toPoint
  812. ) {
  813. pathfinder.connections[k].graphics =
  814. oldConnections[j].graphics;
  815. found = true;
  816. break;
  817. }
  818. }
  819. if (!found) {
  820. oldConnections[j].destroy();
  821. }
  822. }
  823. // Clear obstacles to force recalculation. This must be done on every
  824. // redraw in case positions have changed. Recalculation is handled in
  825. // Connection.getPath on demand.
  826. delete this.chartObstacles;
  827. delete this.lineObstacles;
  828. // Draw the pending connections
  829. pathfinder.renderConnections(deferRender);
  830. },
  831. /**
  832. * Draw the chart's connecting paths.
  833. *
  834. * @function Highcharts.Pathfinder#renderConnections
  835. *
  836. * @param {boolean} deferRender
  837. * Whether or not to defer render until series animation is finished.
  838. * Used on first render.
  839. */
  840. renderConnections: function (deferRender) {
  841. if (deferRender) {
  842. // Render after series are done animating
  843. this.chart.series.forEach(function (series) {
  844. var render = function () {
  845. // Find pathfinder connections belonging to this series
  846. // that haven't rendered, and render them now.
  847. var pathfinder = series.chart.pathfinder,
  848. conns = pathfinder && pathfinder.connections || [];
  849. conns.forEach(function (connection) {
  850. if (
  851. connection.fromPoint &&
  852. connection.fromPoint.series === series
  853. ) {
  854. connection.render();
  855. }
  856. });
  857. if (series.pathfinderRemoveRenderEvent) {
  858. series.pathfinderRemoveRenderEvent();
  859. delete series.pathfinderRemoveRenderEvent;
  860. }
  861. };
  862. if (series.options.animation === false) {
  863. render();
  864. } else {
  865. series.pathfinderRemoveRenderEvent = addEvent(
  866. series, 'afterAnimate', render
  867. );
  868. }
  869. });
  870. } else {
  871. // Go through connections and render them
  872. this.connections.forEach(function (connection) {
  873. connection.render();
  874. });
  875. }
  876. },
  877. /**
  878. * Get obstacles for the points in the chart. Does not include connecting
  879. * lines from Pathfinder. Applies algorithmMargin to the obstacles.
  880. *
  881. * @function Highcharts.Pathfinder#getChartObstacles
  882. *
  883. * @param {object} options
  884. * Options for the calculation. Currenlty only
  885. * options.algorithmMargin.
  886. *
  887. * @return {Array<object>}
  888. * An array of calculated obstacles. Each obstacle is defined as an
  889. * object with xMin, xMax, yMin and yMax properties.
  890. */
  891. getChartObstacles: function (options) {
  892. var obstacles = [],
  893. series = this.chart.series,
  894. margin = pick(options.algorithmMargin, 0),
  895. calculatedMargin;
  896. for (var i = 0, sLen = series.length; i < sLen; ++i) {
  897. if (series[i].visible) {
  898. for (
  899. var j = 0, pLen = series[i].points.length, bb, point;
  900. j < pLen;
  901. ++j
  902. ) {
  903. point = series[i].points[j];
  904. if (point.visible) {
  905. bb = getPointBB(point);
  906. if (bb) {
  907. obstacles.push({
  908. xMin: bb.xMin - margin,
  909. xMax: bb.xMax + margin,
  910. yMin: bb.yMin - margin,
  911. yMax: bb.yMax + margin
  912. });
  913. }
  914. }
  915. }
  916. }
  917. }
  918. // Sort obstacles by xMin for optimization
  919. obstacles = obstacles.sort(function (a, b) {
  920. return a.xMin - b.xMin;
  921. });
  922. // Add auto-calculated margin if the option is not defined
  923. if (!defined(options.algorithmMargin)) {
  924. calculatedMargin =
  925. options.algorithmMargin =
  926. calculateObstacleMargin(obstacles);
  927. obstacles.forEach(function (obstacle) {
  928. obstacle.xMin -= calculatedMargin;
  929. obstacle.xMax += calculatedMargin;
  930. obstacle.yMin -= calculatedMargin;
  931. obstacle.yMax += calculatedMargin;
  932. });
  933. }
  934. return obstacles;
  935. },
  936. /**
  937. * Utility function to get metrics for obstacles:
  938. * - Widest obstacle width
  939. * - Tallest obstacle height
  940. *
  941. * @function Highcharts.Pathfinder#getObstacleMetrics
  942. *
  943. * @param {Array<object>} obstacles
  944. * An array of obstacles to inspect.
  945. *
  946. * @return {object}
  947. * The calculated metrics, as an object with maxHeight and maxWidth
  948. * properties.
  949. */
  950. getObstacleMetrics: function (obstacles) {
  951. var maxWidth = 0,
  952. maxHeight = 0,
  953. width,
  954. height,
  955. i = obstacles.length;
  956. while (i--) {
  957. width = obstacles[i].xMax - obstacles[i].xMin;
  958. height = obstacles[i].yMax - obstacles[i].yMin;
  959. if (maxWidth < width) {
  960. maxWidth = width;
  961. }
  962. if (maxHeight < height) {
  963. maxHeight = height;
  964. }
  965. }
  966. return {
  967. maxHeight: maxHeight,
  968. maxWidth: maxWidth
  969. };
  970. },
  971. /**
  972. * Utility to get which direction to start the pathfinding algorithm
  973. * (X vs Y), calculated from a set of marker options.
  974. *
  975. * @function Highcharts.Pathfinder#getAlgorithmStartDirection
  976. *
  977. * @param {Highcharts.ConnectorsMarkerOptions} markerOptions
  978. * Marker options to calculate from.
  979. *
  980. * @return {boolean}
  981. * Returns true for X, false for Y, and undefined for autocalculate.
  982. */
  983. getAlgorithmStartDirection: function (markerOptions) {
  984. var xCenter = markerOptions.align !== 'left' &&
  985. markerOptions.align !== 'right',
  986. yCenter = markerOptions.verticalAlign !== 'top' &&
  987. markerOptions.verticalAlign !== 'bottom',
  988. undef;
  989. return xCenter ?
  990. (yCenter ? undef : false) : // x is centered
  991. (yCenter ? true : undef); // x is off-center
  992. }
  993. };
  994. // Add to Highcharts namespace
  995. H.Connection = Connection;
  996. H.Pathfinder = Pathfinder;
  997. // Add pathfinding capabilities to Points
  998. extend(H.Point.prototype, /** @lends Point.prototype */ {
  999. /**
  1000. * Get coordinates of anchor point for pathfinder connection.
  1001. *
  1002. * @private
  1003. * @function Highcharts.Point#getPathfinderAnchorPoint
  1004. *
  1005. * @param {Highcharts.ConnectorsMarkerOptions} markerOptions
  1006. * Connection options for position on point.
  1007. *
  1008. * @return {object}
  1009. * An object with x/y properties for the position. Coordinates are
  1010. * in plot values, not relative to point.
  1011. */
  1012. getPathfinderAnchorPoint: function (markerOptions) {
  1013. var bb = getPointBB(this),
  1014. x,
  1015. y;
  1016. switch (markerOptions.align) { // eslint-disable-line default-case
  1017. case 'right':
  1018. x = 'xMax';
  1019. break;
  1020. case 'left':
  1021. x = 'xMin';
  1022. }
  1023. switch (markerOptions.verticalAlign) { // eslint-disable-line default-case
  1024. case 'top':
  1025. y = 'yMin';
  1026. break;
  1027. case 'bottom':
  1028. y = 'yMax';
  1029. }
  1030. return {
  1031. x: x ? bb[x] : (bb.xMin + bb.xMax) / 2,
  1032. y: y ? bb[y] : (bb.yMin + bb.yMax) / 2
  1033. };
  1034. },
  1035. /**
  1036. * Utility to get the angle from one point to another.
  1037. *
  1038. * @private
  1039. * @function Highcharts.Point#getRadiansToVector
  1040. *
  1041. * @param {object} v1
  1042. * The first vector, as an object with x/y properties.
  1043. *
  1044. * @param {object} v2
  1045. * The second vector, as an object with x/y properties.
  1046. *
  1047. * @return {number}
  1048. * The angle in degrees
  1049. */
  1050. getRadiansToVector: function (v1, v2) {
  1051. var box;
  1052. if (!defined(v2)) {
  1053. box = getPointBB(this);
  1054. v2 = {
  1055. x: (box.xMin + box.xMax) / 2,
  1056. y: (box.yMin + box.yMax) / 2
  1057. };
  1058. }
  1059. return Math.atan2(v2.y - v1.y, v1.x - v2.x);
  1060. },
  1061. /**
  1062. * Utility to get the position of the marker, based on the path angle and
  1063. * the marker's radius.
  1064. *
  1065. * @private
  1066. * @function Highcharts.Point#getMarkerVector
  1067. *
  1068. * @param {number} radians
  1069. * The angle in radians from the point center to another vector.
  1070. *
  1071. * @param {number} markerRadius
  1072. * The radius of the marker, to calculate the additional distance to
  1073. * the center of the marker.
  1074. *
  1075. * @param {object} anchor
  1076. * The anchor point of the path and marker as an object with x/y
  1077. * properties.
  1078. *
  1079. * @return {object}
  1080. * The marker vector as an object with x/y properties.
  1081. */
  1082. getMarkerVector: function (radians, markerRadius, anchor) {
  1083. var twoPI = Math.PI * 2.0,
  1084. theta = radians,
  1085. bb = getPointBB(this),
  1086. rectWidth = bb.xMax - bb.xMin,
  1087. rectHeight = bb.yMax - bb.yMin,
  1088. rAtan = Math.atan2(rectHeight, rectWidth),
  1089. tanTheta = 1,
  1090. leftOrRightRegion = false,
  1091. rectHalfWidth = rectWidth / 2.0,
  1092. rectHalfHeight = rectHeight / 2.0,
  1093. rectHorizontalCenter = bb.xMin + rectHalfWidth,
  1094. rectVerticalCenter = bb.yMin + rectHalfHeight,
  1095. edgePoint = {
  1096. x: rectHorizontalCenter,
  1097. y: rectVerticalCenter
  1098. },
  1099. markerPoint = {},
  1100. xFactor = 1,
  1101. yFactor = 1;
  1102. while (theta < -Math.PI) {
  1103. theta += twoPI;
  1104. }
  1105. while (theta > Math.PI) {
  1106. theta -= twoPI;
  1107. }
  1108. tanTheta = Math.tan(theta);
  1109. if ((theta > -rAtan) && (theta <= rAtan)) {
  1110. // Right side
  1111. yFactor = -1;
  1112. leftOrRightRegion = true;
  1113. } else if (theta > rAtan && theta <= (Math.PI - rAtan)) {
  1114. // Top side
  1115. yFactor = -1;
  1116. } else if (theta > (Math.PI - rAtan) || theta <= -(Math.PI - rAtan)) {
  1117. // Left side
  1118. xFactor = -1;
  1119. leftOrRightRegion = true;
  1120. } else {
  1121. // Bottom side
  1122. xFactor = -1;
  1123. }
  1124. // Correct the edgePoint according to the placement of the marker
  1125. if (leftOrRightRegion) {
  1126. edgePoint.x += xFactor * (rectHalfWidth);
  1127. edgePoint.y += yFactor * (rectHalfWidth) * tanTheta;
  1128. } else {
  1129. edgePoint.x += xFactor * (rectHeight / (2.0 * tanTheta));
  1130. edgePoint.y += yFactor * (rectHalfHeight);
  1131. }
  1132. if (anchor.x !== rectHorizontalCenter) {
  1133. edgePoint.x = anchor.x;
  1134. }
  1135. if (anchor.y !== rectVerticalCenter) {
  1136. edgePoint.y = anchor.y;
  1137. }
  1138. markerPoint.x = edgePoint.x + (markerRadius * Math.cos(theta));
  1139. markerPoint.y = edgePoint.y - (markerRadius * Math.sin(theta));
  1140. return markerPoint;
  1141. }
  1142. });
  1143. // Warn if using legacy options. Copy the options over. Note that this will
  1144. // still break if using the legacy options in chart.update, addSeries etc.
  1145. function warnLegacy(chart) {
  1146. if (
  1147. chart.options.pathfinder ||
  1148. chart.series.reduce(function (acc, series) {
  1149. if (series.options) {
  1150. merge(
  1151. true,
  1152. (
  1153. series.options.connectors = series.options.connectors ||
  1154. {}
  1155. ), series.options.pathfinder
  1156. );
  1157. }
  1158. return acc || series.options && series.options.pathfinder;
  1159. }, false)
  1160. ) {
  1161. merge(
  1162. true,
  1163. (chart.options.connectors = chart.options.connectors || {}),
  1164. chart.options.pathfinder
  1165. );
  1166. H.error('WARNING: Pathfinder options have been renamed. ' +
  1167. 'Use "chart.connectors" or "series.connectors" instead.');
  1168. }
  1169. }
  1170. // Initialize Pathfinder for charts
  1171. H.Chart.prototype.callbacks.push(function (chart) {
  1172. var options = chart.options;
  1173. if (options.connectors.enabled !== false) {
  1174. warnLegacy(chart);
  1175. this.pathfinder = new Pathfinder(this);
  1176. this.pathfinder.update(true); // First draw, defer render
  1177. }
  1178. });