networkgraph.src.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. /**
  2. * Networkgraph series
  3. *
  4. * (c) 2010-2019 Paweł Fus
  5. *
  6. * License: www.highcharts.com/license
  7. */
  8. 'use strict';
  9. import H from '../../parts/Globals.js';
  10. import '../../parts/Utilities.js';
  11. import '../../parts/Options.js';
  12. import '../../mixins/nodes.js';
  13. import '/layouts.js';
  14. var addEvent = H.addEvent,
  15. defined = H.defined,
  16. seriesType = H.seriesType,
  17. seriesTypes = H.seriesTypes,
  18. pick = H.pick,
  19. Chart = H.Chart,
  20. Point = H.Point,
  21. Series = H.Series;
  22. /**
  23. * A networkgraph is a type of relationship chart, where connnections
  24. * (links) attracts nodes (points) and other nodes repulse each other.
  25. *
  26. * @extends plotOptions.line
  27. * @product highcharts
  28. * @sample highcharts/demo/network-graph/
  29. * Networkgraph
  30. * @since 7.0.0
  31. * @excluding boostThreshold, animation, animationLimit, connectEnds,
  32. * connectNulls, dragDrop, getExtremesFromAll, label, linecap,
  33. * negativeColor, pointInterval, pointIntervalUnit,
  34. * pointPlacement, pointStart, softThreshold, stack, stacking,
  35. * step, threshold, xAxis, yAxis, zoneAxis
  36. * @optionparent plotOptions.networkgraph
  37. */
  38. seriesType('networkgraph', 'line', {
  39. marker: {
  40. enabled: true
  41. },
  42. dataLabels: {
  43. format: '{key}'
  44. },
  45. /**
  46. * Link style options
  47. */
  48. link: {
  49. /**
  50. * A name for the dash style to use for links.
  51. *
  52. * @type {String}
  53. * @apioption plotOptions.networkgraph.link.dashStyle
  54. * @defaults undefined
  55. */
  56. /**
  57. * Color of the link between two nodes.
  58. */
  59. color: 'rgba(100, 100, 100, 0.5)',
  60. /**
  61. * Width (px) of the link between two nodes.
  62. */
  63. width: 1
  64. },
  65. /**
  66. * Flag to determine if nodes are draggable or not.
  67. */
  68. draggable: true,
  69. layoutAlgorithm: {
  70. /**
  71. * Ideal length (px) of the link between two nodes. When not defined,
  72. * length is calculated as:
  73. * `Math.pow(availableWidth * availableHeight / nodesLength, 0.4);`
  74. *
  75. * Note: Because of the algorithm specification, length of each link
  76. * might be not exactly as specified.
  77. *
  78. * @type {number}
  79. * @apioption series.networkgraph.layoutAlgorithm.linkLength
  80. * @sample highcharts/series-networkgraph/styled-links/
  81. * Numerical values
  82. * @defaults undefined
  83. */
  84. /**
  85. * Initial layout algorithm for positioning nodes. Can be one of
  86. * built-in options ("circle", "random") or a function where positions
  87. * should be set on each node (`this.nodes`) as `node.plotX` and
  88. * `node.plotY`
  89. *
  90. * @sample highcharts/series-networkgraph/initial-positions/
  91. * Initial positions with callback
  92. * @type {String|Function}
  93. * @validvalue ["circle", "random"]
  94. */
  95. initialPositions: 'circle',
  96. /**
  97. * Experimental. Enables live simulation of the algorithm
  98. * implementation. All nodes are animated as the forces applies on
  99. * them.
  100. *
  101. * @sample highcharts/demo/network-graph/
  102. * Live simulation enabled
  103. */
  104. enableSimulation: false,
  105. /**
  106. * Type of the algorithm used when positioning nodes.
  107. *
  108. * @validvalue ["reingold-fruchterman"]
  109. */
  110. type: 'reingold-fruchterman',
  111. /**
  112. * Max number of iterations before algorithm will stop. In general,
  113. * algorithm should find positions sooner, but when rendering huge
  114. * number of nodes, it is recommended to increase this value as
  115. * finding perfect graph positions can require more time.
  116. */
  117. maxIterations: 1000,
  118. /**
  119. * Gravitational const used in the barycenter force of the algorithm.
  120. *
  121. * @sample highcharts/series-networkgraph/forces/
  122. * Custom forces
  123. */
  124. gravitationalConstant: 0.0625,
  125. /**
  126. * Friction applied on forces to prevent nodes rushing to fast to the
  127. * desired positions.
  128. */
  129. friction: -0.981,
  130. /**
  131. * Repulsive force applied on a node. Passed are two arguments:
  132. * - `d` - which is current distance between two nodes
  133. * - `k` - which is desired distance between two nodes
  134. *
  135. * @sample highcharts/series-networkgraph/forces/
  136. * Custom forces
  137. * @type {Function}
  138. * @default function (d, k) { return k * k / d; }
  139. */
  140. repulsiveForce: function (d, k) {
  141. /*
  142. basic, not recommended:
  143. return k / d;
  144. */
  145. /*
  146. standard:
  147. return k * k / d;
  148. */
  149. /*
  150. grid-variant:
  151. return k * k / d * (2 * k - d > 0 ? 1 : 0);
  152. */
  153. return k * k / d;
  154. },
  155. /**
  156. * Attraction force applied on a node which is conected to another node
  157. * by a link. Passed are two arguments:
  158. * - `d` - which is current distance between two nodes
  159. * - `k` - which is desired distance between two nodes
  160. *
  161. * @sample highcharts/series-networkgraph/forces/
  162. * Custom forces
  163. * @type {Function}
  164. * @default function (d, k) { return k * k / d; }
  165. */
  166. attractiveForce: function (d, k) {
  167. /*
  168. basic, not recommended:
  169. return d / k;
  170. */
  171. return d * d / k;
  172. }
  173. },
  174. showInLegend: false
  175. }, {
  176. isNetworkgraph: true,
  177. drawGraph: null,
  178. isCartesian: false,
  179. requireSorting: false,
  180. directTouch: true,
  181. noSharedTooltip: true,
  182. trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
  183. drawTracker: H.TrackerMixin.drawTrackerPoint,
  184. // Animation is run in `series.simulation`.
  185. animate: null,
  186. /**
  187. * Create a single node that holds information on incoming and outgoing
  188. * links.
  189. */
  190. createNode: H.NodesMixin.createNode,
  191. /**
  192. * Extend generatePoints by adding the nodes, which are Point objects
  193. * but pushed to the this.nodes array.
  194. */
  195. generatePoints: function () {
  196. var nodeLookup = {},
  197. chart = this.chart;
  198. H.Series.prototype.generatePoints.call(this);
  199. if (!this.nodes) {
  200. this.nodes = []; // List of Point-like node items
  201. }
  202. this.colorCounter = 0;
  203. // Reset links from previous run
  204. this.nodes.forEach(function (node) {
  205. node.linksFrom.length = 0;
  206. node.linksTo.length = 0;
  207. });
  208. // Create the node list and set up links
  209. this.points.forEach(function (point) {
  210. if (defined(point.from)) {
  211. if (!nodeLookup[point.from]) {
  212. nodeLookup[point.from] = this.createNode(point.from);
  213. }
  214. nodeLookup[point.from].linksFrom.push(point);
  215. point.fromNode = nodeLookup[point.from];
  216. // Point color defaults to the fromNode's color
  217. if (chart.styledMode) {
  218. point.colorIndex = pick(
  219. point.options.colorIndex,
  220. nodeLookup[point.from].colorIndex
  221. );
  222. } else {
  223. point.color =
  224. point.options.color || nodeLookup[point.from].color;
  225. }
  226. }
  227. if (defined(point.to)) {
  228. if (!nodeLookup[point.to]) {
  229. nodeLookup[point.to] = this.createNode(point.to);
  230. }
  231. nodeLookup[point.to].linksTo.push(point);
  232. point.toNode = nodeLookup[point.to];
  233. }
  234. point.name = point.name || point.id; // for use in formats
  235. }, this);
  236. if (this.options.nodes) {
  237. this.options.nodes.forEach(
  238. function (nodeOptions) {
  239. if (!nodeLookup[nodeOptions.id]) {
  240. nodeLookup[nodeOptions.id] = this
  241. .createNode(nodeOptions.id);
  242. }
  243. },
  244. this
  245. );
  246. }
  247. },
  248. /**
  249. * Run pre-translation by generating the nodeColumns.
  250. */
  251. translate: function () {
  252. if (!this.processedXData) {
  253. this.processData();
  254. }
  255. this.generatePoints();
  256. this.deferLayout();
  257. this.nodes.forEach(function (node) {
  258. // Draw the links from this node
  259. node.isInside = true;
  260. node.linksFrom.forEach(function (point) {
  261. point.shapeType = 'path';
  262. // Pass test in drawPoints
  263. point.y = 1;
  264. });
  265. });
  266. },
  267. deferLayout: function () {
  268. var layoutOptions = this.options.layoutAlgorithm,
  269. graphLayoutsStorage = this.chart.graphLayoutsStorage,
  270. chartOptions = this.chart.options.chart,
  271. layout;
  272. if (!this.visible) {
  273. return;
  274. }
  275. if (!graphLayoutsStorage) {
  276. this.chart.graphLayoutsStorage = graphLayoutsStorage = {};
  277. }
  278. layout = graphLayoutsStorage[layoutOptions.type];
  279. if (!layout) {
  280. layoutOptions.enableSimulation = !defined(chartOptions.forExport) ?
  281. layoutOptions.enableSimulation :
  282. !chartOptions.forExport;
  283. graphLayoutsStorage[layoutOptions.type] = layout =
  284. new H.layouts[layoutOptions.type](layoutOptions);
  285. }
  286. this.layout = layout;
  287. layout.setArea(0, 0, this.chart.plotWidth, this.chart.plotHeight);
  288. layout.addSeries(this);
  289. layout.addNodes(this.nodes);
  290. layout.addLinks(this.points);
  291. },
  292. /**
  293. * Extend the render function to also render this.nodes together with
  294. * the points.
  295. */
  296. render: function () {
  297. var points = this.points,
  298. hoverPoint = this.chart.hoverPoint,
  299. dataLabels = [];
  300. // Render markers:
  301. this.points = this.nodes;
  302. seriesTypes.line.prototype.render.call(this);
  303. this.points = points;
  304. points.forEach(function (point) {
  305. if (point.fromNode && point.toNode) {
  306. point.renderLink();
  307. point.redrawLink();
  308. }
  309. });
  310. if (hoverPoint && hoverPoint.series === this) {
  311. this.redrawHalo(hoverPoint);
  312. }
  313. this.nodes.forEach(function (node) {
  314. if (node.dataLabel) {
  315. dataLabels.push(node.dataLabel);
  316. }
  317. });
  318. H.Chart.prototype.hideOverlappingLabels(dataLabels);
  319. },
  320. /*
  321. * Draggable mode:
  322. */
  323. redrawHalo: function (point) {
  324. if (point && this.halo) {
  325. this.halo.attr({
  326. d: point.haloPath(
  327. this.options.states.hover.halo.size
  328. )
  329. });
  330. }
  331. },
  332. onMouseDown: function (point, event) {
  333. var normalizedEvent = this.chart.pointer.normalize(event);
  334. point.fixedPosition = {
  335. chartX: normalizedEvent.chartX,
  336. chartY: normalizedEvent.chartY,
  337. plotX: point.plotX,
  338. plotY: point.plotY
  339. };
  340. },
  341. onMouseMove: function (point, event) {
  342. if (point.fixedPosition) {
  343. var series = this,
  344. chart = series.chart,
  345. normalizedEvent = chart.pointer.normalize(event),
  346. diffX = point.fixedPosition.chartX - normalizedEvent.chartX,
  347. diffY = point.fixedPosition.chartY - normalizedEvent.chartY,
  348. newPlotX,
  349. newPlotY;
  350. // At least 5px to apply change (avoids simple click):
  351. if (Math.abs(diffX) > 5 || Math.abs(diffY) > 5) {
  352. newPlotX = point.fixedPosition.plotX - diffX;
  353. newPlotY = point.fixedPosition.plotY - diffY;
  354. if (chart.isInsidePlot(newPlotX, newPlotY)) {
  355. point.plotX = newPlotX;
  356. point.plotY = newPlotY;
  357. series.redrawHalo();
  358. if (!series.layout.simulation) {
  359. // Start new simulation:
  360. if (!series.layout.enableSimulation) {
  361. // Run only one iteration to speed things up:
  362. series.layout.setMaxIterations(1);
  363. }
  364. // When dragging nodes, we don't need to calculate
  365. // initial positions and rendering nodes:
  366. series.layout.setInitialRendering(false);
  367. series.layout.run();
  368. // Restore defaults:
  369. series.layout.setInitialRendering(true);
  370. } else {
  371. // Extend current simulation:
  372. series.layout.resetSimulation();
  373. }
  374. }
  375. }
  376. }
  377. },
  378. onMouseUp: function (point) {
  379. if (point.fixedPosition) {
  380. this.layout.run();
  381. delete point.fixedPosition;
  382. }
  383. },
  384. destroy: function () {
  385. this.nodes.forEach(function (node) {
  386. node.destroy();
  387. });
  388. return Series.prototype.destroy.apply(this, arguments);
  389. }
  390. }, {
  391. getDegree: function () {
  392. var deg = this.isNode ? this.linksFrom.length + this.linksTo.length : 0;
  393. return deg === 0 ? 1 : deg;
  394. },
  395. // Links:
  396. getLinkAttribues: function () {
  397. var linkOptions = this.series.options.link,
  398. pointOptions = this.options;
  399. return {
  400. 'stroke-width': pick(pointOptions.width, linkOptions.width),
  401. stroke: pointOptions.color || linkOptions.color,
  402. dashstyle: pointOptions.dashStyle || linkOptions.dashStyle
  403. };
  404. },
  405. renderLink: function () {
  406. if (!this.graphic) {
  407. this.graphic = this.series.chart.renderer
  408. .path(
  409. this.getLinkPath(this.fromNode, this.toNode)
  410. )
  411. .attr(this.getLinkAttribues())
  412. .add(this.series.group);
  413. }
  414. },
  415. redrawLink: function () {
  416. if (this.graphic) {
  417. this.graphic.animate({
  418. d: this.getLinkPath(this.fromNode, this.toNode)
  419. });
  420. }
  421. },
  422. getLinkPath: function (from, to) {
  423. return [
  424. 'M',
  425. from.plotX,
  426. from.plotY,
  427. 'L',
  428. to.plotX,
  429. to.plotY
  430. ];
  431. /*
  432. IDEA: different link shapes?
  433. return [
  434. 'M',
  435. from.plotX,
  436. from.plotY,
  437. 'Q',
  438. (to.plotX + from.plotX) / 2,
  439. (to.plotY + from.plotY) / 2 + 15,
  440. to.plotX,
  441. to.plotY
  442. ];*/
  443. },
  444. // Default utils:
  445. destroy: function () {
  446. if (this.isNode) {
  447. this.linksFrom.forEach(
  448. function (linkFrom) {
  449. if (linkFrom.graphic) {
  450. linkFrom.graphic = linkFrom.graphic.destroy();
  451. }
  452. }
  453. );
  454. }
  455. return Point.prototype.destroy.apply(this, arguments);
  456. }
  457. });
  458. addEvent(seriesTypes.networkgraph, 'updatedData', function () {
  459. if (this.layout) {
  460. this.layout.stop();
  461. }
  462. });
  463. addEvent(seriesTypes.networkgraph.prototype.pointClass, 'remove', function () {
  464. if (this.series.layout) {
  465. if (this.isNode) {
  466. this.series.layout.removeNode(this);
  467. } else {
  468. this.series.layout.removeLink(this);
  469. }
  470. }
  471. });
  472. /*
  473. * Multiple series support:
  474. */
  475. // Clear previous layouts
  476. addEvent(Chart, 'predraw', function () {
  477. if (this.graphLayoutsStorage) {
  478. H.objectEach(
  479. this.graphLayoutsStorage,
  480. function (layout) {
  481. layout.stop();
  482. }
  483. );
  484. }
  485. });
  486. addEvent(Chart, 'render', function () {
  487. if (this.graphLayoutsStorage) {
  488. H.setAnimation(false, this);
  489. H.objectEach(
  490. this.graphLayoutsStorage,
  491. function (layout) {
  492. layout.run();
  493. }
  494. );
  495. }
  496. });
  497. /*
  498. * Draggable mode:
  499. */
  500. addEvent(
  501. seriesTypes.networkgraph.prototype.pointClass,
  502. 'mouseOver',
  503. function () {
  504. H.css(this.series.chart.container, { cursor: 'move' });
  505. }
  506. );
  507. addEvent(
  508. seriesTypes.networkgraph.prototype.pointClass,
  509. 'mouseOut',
  510. function () {
  511. H.css(this.series.chart.container, { cursor: 'default' });
  512. }
  513. );
  514. addEvent(
  515. Chart,
  516. 'load',
  517. function () {
  518. var chart = this,
  519. unbinders = [];
  520. if (chart.container) {
  521. unbinders.push(
  522. addEvent(
  523. chart.container,
  524. 'mousedown',
  525. function (event) {
  526. var point = chart.hoverPoint;
  527. if (
  528. point &&
  529. point.series &&
  530. point.series.isNetworkgraph &&
  531. point.series.options.draggable
  532. ) {
  533. point.series.onMouseDown(point, event);
  534. unbinders.push(addEvent(
  535. chart.container,
  536. 'mousemove',
  537. function (e) {
  538. return point.series.onMouseMove(point, e);
  539. }
  540. ));
  541. unbinders.push(addEvent(
  542. chart.container.ownerDocument,
  543. 'mouseup',
  544. function (e) {
  545. return point.series.onMouseUp(point, e);
  546. }
  547. ));
  548. }
  549. }
  550. )
  551. );
  552. }
  553. addEvent(chart, 'destroy', function () {
  554. unbinders.forEach(function (unbind) {
  555. unbind();
  556. });
  557. });
  558. }
  559. );
  560. /**
  561. * A `networkgraph` series. If the [type](#series.networkgraph.type) option is
  562. * not specified, it is inherited from [chart.type](#chart.type).
  563. *
  564. * @type {Object}
  565. * @extends series,plotOptions.networkgraph
  566. * @excluding boostThreshold, animation, animationLimit, connectEnds,
  567. * connectNulls, dragDrop, getExtremesFromAll, label, linecap,
  568. * negativeColor, pointInterval, pointIntervalUnit,
  569. * pointPlacement, pointStart, softThreshold, stack, stacking,
  570. * step, threshold, xAxis, yAxis, zoneAxis
  571. * @product highcharts
  572. * @apioption series.networkgraph
  573. */
  574. /**
  575. * An array of data points for the series. For the `networkgraph` series type,
  576. * points can be given in the following way:
  577. *
  578. * An array of objects with named values. The following snippet shows only a
  579. * few settings, see the complete options set below. If the total number of
  580. * data points exceeds the series'
  581. * [turboThreshold](#series.area.turboThreshold), this option is not available.
  582. *
  583. * ```js
  584. * data: [{
  585. * from: 'Category1',
  586. * to: 'Category2'
  587. * }, {
  588. * from: 'Category1',
  589. * to: 'Category3'
  590. * }]
  591. * ```
  592. *
  593. * @type {Array<Object|Array|Number>}
  594. * @extends series.line.data
  595. * @excluding drilldown,marker,x,y,draDrop
  596. * @sample {highcharts} highcharts/chart/reflow-true/
  597. * Numerical values
  598. * @sample {highcharts} highcharts/series/data-array-of-arrays/
  599. * Arrays of numeric x and y
  600. * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
  601. * Arrays of datetime x and y
  602. * @sample {highcharts} highcharts/series/data-array-of-name-value/
  603. * Arrays of point.name and y
  604. * @sample {highcharts} highcharts/series/data-array-of-objects/
  605. * Config objects
  606. * @product highcharts
  607. * @apioption series.networkgraph.data
  608. */
  609. /**
  610. * The node that the link runs from.
  611. *
  612. * @type {String}
  613. * @product highcharts
  614. * @apioption series.networkgraph.data.from
  615. */
  616. /**
  617. * The node that the link runs to.
  618. *
  619. * @type {String}
  620. * @product highcharts
  621. * @apioption series.networkgraph.data.to
  622. */
  623. /**
  624. * The weight of the link.
  625. *
  626. * @type {Number}
  627. * @product highcharts
  628. * @apioption series.networkgraph.data.weight
  629. */
  630. /**
  631. * A collection of options for the individual nodes. The nodes in a
  632. * networkgraph diagram are auto-generated instances of `Highcharts.Point`,
  633. * but options can be applied here and linked by the `id`.
  634. *
  635. * @sample highcharts/series-networkgraph/data-options/
  636. * Networkgraph diagram with node options
  637. *
  638. * @type {Array<*>}
  639. * @product highcharts
  640. * @apioption series.networkgraph.nodes
  641. */
  642. /**
  643. * The id of the auto-generated node, refering to the `from` or `to` setting of
  644. * the link.
  645. *
  646. * @type {string}
  647. * @product highcharts
  648. * @apioption series.networkgraph.nodes.id
  649. */
  650. /**
  651. * The color of the auto generated node.
  652. *
  653. * @type {Highcharts.ColorString}
  654. * @product highcharts
  655. * @apioption series.networkgraph.nodes.color
  656. */
  657. /**
  658. * The color index of the auto generated node, especially for use in styled
  659. * mode.
  660. *
  661. * @type {number}
  662. * @product highcharts
  663. * @apioption series.networkgraph.nodes.colorIndex
  664. */
  665. /**
  666. * The name to display for the node in data labels and tooltips. Use this when
  667. * the name is different from the `id`. Where the id must be unique for each
  668. * node, this is not necessary for the name.
  669. *
  670. * @sample highcharts/series-networkgraph/data-options/
  671. * Networkgraph diagram with node options
  672. *
  673. * @type {string}
  674. * @product highcharts
  675. * @apioption series.networkgraph.nodes.name
  676. */