Pathfinder.js 40 KB

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