OrganizationSeries.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. /* *
  2. *
  3. * Organization chart module
  4. *
  5. * (c) 2018-2020 Torstein Honsi
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import H from '../Core/Globals.js';
  14. import U from '../Core/Utilities.js';
  15. var css = U.css, pick = U.pick, seriesType = U.seriesType, wrap = U.wrap;
  16. /**
  17. * Layout value for the child nodes in an organization chart. If `hanging`, this
  18. * node's children will hang below their parent, allowing a tighter packing of
  19. * nodes in the diagram.
  20. *
  21. * @typedef {"normal"|"hanging"} Highcharts.SeriesOrganizationNodesLayoutValue
  22. */
  23. var base = H.seriesTypes.sankey.prototype;
  24. /**
  25. * @private
  26. * @class
  27. * @name Highcharts.seriesTypes.organization
  28. *
  29. * @augments Highcharts.seriesTypes.sankey
  30. */
  31. seriesType('organization', 'sankey',
  32. /**
  33. * An organization chart is a diagram that shows the structure of an
  34. * organization and the relationships and relative ranks of its parts and
  35. * positions.
  36. *
  37. * @sample highcharts/demo/organization-chart/
  38. * Organization chart
  39. * @sample highcharts/series-organization/horizontal/
  40. * Horizontal organization chart
  41. * @sample highcharts/series-organization/borderless
  42. * Borderless design
  43. * @sample highcharts/series-organization/center-layout
  44. * Centered layout
  45. *
  46. * @extends plotOptions.sankey
  47. * @excluding allowPointSelect, curveFactor, dataSorting
  48. * @since 7.1.0
  49. * @product highcharts
  50. * @requires modules/organization
  51. * @optionparent plotOptions.organization
  52. */
  53. {
  54. /**
  55. * The border color of the node cards.
  56. *
  57. * @type {Highcharts.ColorString}
  58. * @private
  59. */
  60. borderColor: '#666666',
  61. /**
  62. * The border radius of the node cards.
  63. *
  64. * @private
  65. */
  66. borderRadius: 3,
  67. /**
  68. * Radius for the rounded corners of the links between nodes.
  69. *
  70. * @sample highcharts/series-organization/link-options
  71. * Square links
  72. *
  73. * @private
  74. */
  75. linkRadius: 10,
  76. borderWidth: 1,
  77. /**
  78. * @declare Highcharts.SeriesOrganizationDataLabelsOptionsObject
  79. *
  80. * @private
  81. */
  82. dataLabels: {
  83. /* eslint-disable valid-jsdoc */
  84. /**
  85. * A callback for defining the format for _nodes_ in the
  86. * organization chart. The `nodeFormat` option takes precedence
  87. * over `nodeFormatter`.
  88. *
  89. * In an organization chart, the `nodeFormatter` is a quite complex
  90. * function of the available options, striving for a good default
  91. * layout of cards with or without images. In organization chart,
  92. * the data labels come with `useHTML` set to true, meaning they
  93. * will be rendered as true HTML above the SVG.
  94. *
  95. * @sample highcharts/series-organization/datalabels-nodeformatter
  96. * Modify the default label format output
  97. *
  98. * @type {Highcharts.SeriesSankeyDataLabelsFormatterCallbackFunction}
  99. * @since 6.0.2
  100. */
  101. nodeFormatter: function () {
  102. var outerStyle = {
  103. width: '100%',
  104. height: '100%',
  105. display: 'flex',
  106. 'flex-direction': 'row',
  107. 'align-items': 'center',
  108. 'justify-content': 'center'
  109. }, imageStyle = {
  110. 'max-height': '100%',
  111. 'border-radius': '50%'
  112. }, innerStyle = {
  113. width: '100%',
  114. padding: 0,
  115. 'text-align': 'center',
  116. 'white-space': 'normal'
  117. }, nameStyle = {
  118. margin: 0
  119. }, titleStyle = {
  120. margin: 0
  121. }, descriptionStyle = {
  122. opacity: 0.75,
  123. margin: '5px'
  124. };
  125. // eslint-disable-next-line valid-jsdoc
  126. /**
  127. * @private
  128. */
  129. function styleAttr(style) {
  130. return Object.keys(style).reduce(function (str, key) {
  131. return str + key + ':' + style[key] + ';';
  132. }, 'style="') + '"';
  133. }
  134. if (this.point.image) {
  135. imageStyle['max-width'] = '30%';
  136. innerStyle.width = '70%';
  137. }
  138. // PhantomJS doesn't support flex, roll back to absolute
  139. // positioning
  140. if (this.series.chart.renderer.forExport) {
  141. outerStyle.display = 'block';
  142. innerStyle.position = 'absolute';
  143. innerStyle.left = this.point.image ? '30%' : 0;
  144. innerStyle.top = 0;
  145. }
  146. var html = '<div ' + styleAttr(outerStyle) + '>';
  147. if (this.point.image) {
  148. html += '<img src="' + this.point.image + '" ' +
  149. styleAttr(imageStyle) + '>';
  150. }
  151. html += '<div ' + styleAttr(innerStyle) + '>';
  152. if (this.point.name) {
  153. html += '<h4 ' + styleAttr(nameStyle) + '>' +
  154. this.point.name + '</h4>';
  155. }
  156. if (this.point.title) {
  157. html += '<p ' + styleAttr(titleStyle) + '>' +
  158. (this.point.title || '') + '</p>';
  159. }
  160. if (this.point.description) {
  161. html += '<p ' + styleAttr(descriptionStyle) + '>' +
  162. this.point.description + '</p>';
  163. }
  164. html += '</div>' +
  165. '</div>';
  166. return html;
  167. },
  168. /* eslint-enable valid-jsdoc */
  169. style: {
  170. /** @internal */
  171. fontWeight: 'normal',
  172. /** @internal */
  173. fontSize: '13px'
  174. },
  175. useHTML: true
  176. },
  177. /**
  178. * The indentation in pixels of hanging nodes, nodes which parent has
  179. * [layout](#series.organization.nodes.layout) set to `hanging`.
  180. *
  181. * @private
  182. */
  183. hangingIndent: 20,
  184. /**
  185. * The color of the links between nodes.
  186. *
  187. * @type {Highcharts.ColorString}
  188. * @private
  189. */
  190. linkColor: '#666666',
  191. /**
  192. * The line width of the links connecting nodes, in pixels.
  193. *
  194. * @sample highcharts/series-organization/link-options
  195. * Square links
  196. *
  197. * @private
  198. */
  199. linkLineWidth: 1,
  200. /**
  201. * In a horizontal chart, the width of the nodes in pixels. Node that
  202. * most organization charts are vertical, so the name of this option
  203. * is counterintuitive.
  204. *
  205. * @private
  206. */
  207. nodeWidth: 50,
  208. tooltip: {
  209. nodeFormat: '{point.name}<br>{point.title}<br>{point.description}'
  210. }
  211. }, {
  212. pointAttribs: function (point, state) {
  213. var series = this, attribs = base.pointAttribs.call(series, point, state), level = point.isNode ? point.level : point.fromNode.level, levelOptions = series.mapOptionsToLevel[level || 0] || {}, options = point.options, stateOptions = (levelOptions.states && levelOptions.states[state]) || {}, values = ['borderRadius', 'linkColor', 'linkLineWidth']
  214. .reduce(function (obj, key) {
  215. obj[key] = pick(stateOptions[key], options[key], levelOptions[key], series.options[key]);
  216. return obj;
  217. }, {});
  218. if (!point.isNode) {
  219. attribs.stroke = values.linkColor;
  220. attribs['stroke-width'] = values.linkLineWidth;
  221. delete attribs.fill;
  222. }
  223. else {
  224. if (values.borderRadius) {
  225. attribs.r = values.borderRadius;
  226. }
  227. }
  228. return attribs;
  229. },
  230. createNode: function (id) {
  231. var node = base.createNode
  232. .call(this, id);
  233. // All nodes in an org chart are equal width
  234. node.getSum = function () {
  235. return 1;
  236. };
  237. return node;
  238. },
  239. createNodeColumn: function () {
  240. var column = base.createNodeColumn.call(this);
  241. // Wrap the offset function so that the hanging node's children are
  242. // aligned to their parent
  243. wrap(column, 'offset', function (proceed, node, factor) {
  244. var offset = proceed.call(this, node, factor); // eslint-disable-line no-invalid-this
  245. // Modify the default output if the parent's layout is 'hanging'
  246. if (node.hangsFrom) {
  247. return {
  248. absoluteTop: node.hangsFrom.nodeY
  249. };
  250. }
  251. return offset;
  252. });
  253. return column;
  254. },
  255. translateNode: function (node, column) {
  256. base.translateNode.call(this, node, column);
  257. if (node.hangsFrom) {
  258. node.shapeArgs.height -=
  259. this.options.hangingIndent;
  260. if (!this.chart.inverted) {
  261. node.shapeArgs.y += this.options.hangingIndent;
  262. }
  263. }
  264. node.nodeHeight = this.chart.inverted ?
  265. node.shapeArgs.width :
  266. node.shapeArgs.height;
  267. },
  268. // General function to apply corner radius to a path - can be lifted to
  269. // renderer or utilities if we need it elsewhere.
  270. curvedPath: function (path, r) {
  271. var d = [];
  272. for (var i = 0; i < path.length; i++) {
  273. var x = path[i][1];
  274. var y = path[i][2];
  275. if (typeof x === 'number' && typeof y === 'number') {
  276. // moveTo
  277. if (i === 0) {
  278. d.push(['M', x, y]);
  279. }
  280. else if (i === path.length - 1) {
  281. d.push(['L', x, y]);
  282. // curveTo
  283. }
  284. else if (r) {
  285. var prevSeg = path[i - 1];
  286. var nextSeg = path[i + 1];
  287. if (prevSeg && nextSeg) {
  288. var x1 = prevSeg[1], y1 = prevSeg[2], x2 = nextSeg[1], y2 = nextSeg[2];
  289. // Only apply to breaks
  290. if (typeof x1 === 'number' &&
  291. typeof x2 === 'number' &&
  292. typeof y1 === 'number' &&
  293. typeof y2 === 'number' &&
  294. x1 !== x2 &&
  295. y1 !== y2) {
  296. var directionX = x1 < x2 ? 1 : -1, directionY = y1 < y2 ? 1 : -1;
  297. d.push([
  298. 'L',
  299. x - directionX * Math.min(Math.abs(x - x1), r),
  300. y - directionY * Math.min(Math.abs(y - y1), r)
  301. ], [
  302. 'C',
  303. x,
  304. y,
  305. x,
  306. y,
  307. x + directionX * Math.min(Math.abs(x - x2), r),
  308. y + directionY * Math.min(Math.abs(y - y2), r)
  309. ]);
  310. }
  311. }
  312. // lineTo
  313. }
  314. else {
  315. d.push(['L', x, y]);
  316. }
  317. }
  318. }
  319. return d;
  320. },
  321. translateLink: function (point) {
  322. var fromNode = point.fromNode, toNode = point.toNode, crisp = Math.round(this.options.linkLineWidth) % 2 / 2, x1 = Math.floor(fromNode.shapeArgs.x +
  323. fromNode.shapeArgs.width) + crisp, y1 = Math.floor(fromNode.shapeArgs.y +
  324. fromNode.shapeArgs.height / 2) + crisp, x2 = Math.floor(toNode.shapeArgs.x) + crisp, y2 = Math.floor(toNode.shapeArgs.y +
  325. toNode.shapeArgs.height / 2) + crisp, xMiddle, hangingIndent = this.options.hangingIndent, toOffset = toNode.options.offset, percentOffset = /%$/.test(toOffset) && parseInt(toOffset, 10), inverted = this.chart.inverted;
  326. if (inverted) {
  327. x1 -= fromNode.shapeArgs.width;
  328. x2 += toNode.shapeArgs.width;
  329. }
  330. xMiddle = Math.floor(x2 +
  331. (inverted ? 1 : -1) *
  332. (this.colDistance - this.nodeWidth) / 2) + crisp;
  333. // Put the link on the side of the node when an offset is given. HR
  334. // node in the main demo.
  335. if (percentOffset &&
  336. (percentOffset >= 50 || percentOffset <= -50)) {
  337. xMiddle = x2 = Math.floor(x2 + (inverted ? -0.5 : 0.5) *
  338. toNode.shapeArgs.width) + crisp;
  339. y2 = toNode.shapeArgs.y;
  340. if (percentOffset > 0) {
  341. y2 += toNode.shapeArgs.height;
  342. }
  343. }
  344. if (toNode.hangsFrom === fromNode) {
  345. if (this.chart.inverted) {
  346. y1 = Math.floor(fromNode.shapeArgs.y +
  347. fromNode.shapeArgs.height -
  348. hangingIndent / 2) + crisp;
  349. y2 = (toNode.shapeArgs.y +
  350. toNode.shapeArgs.height);
  351. }
  352. else {
  353. y1 = Math.floor(fromNode.shapeArgs.y +
  354. hangingIndent / 2) + crisp;
  355. }
  356. xMiddle = x2 = Math.floor(toNode.shapeArgs.x +
  357. toNode.shapeArgs.width / 2) + crisp;
  358. }
  359. point.plotY = 1;
  360. point.shapeType = 'path';
  361. point.shapeArgs = {
  362. d: this.curvedPath([
  363. ['M', x1, y1],
  364. ['L', xMiddle, y1],
  365. ['L', xMiddle, y2],
  366. ['L', x2, y2]
  367. ], this.options.linkRadius)
  368. };
  369. },
  370. alignDataLabel: function (point, dataLabel, options) {
  371. // Align the data label to the point graphic
  372. if (options.useHTML) {
  373. var width = point.shapeArgs.width, height = point.shapeArgs.height, padjust = (this.options.borderWidth +
  374. 2 * this.options.dataLabels.padding);
  375. if (this.chart.inverted) {
  376. width = height;
  377. height = point.shapeArgs.width;
  378. }
  379. height -= padjust;
  380. width -= padjust;
  381. // Set the size of the surrounding div emulating `g`
  382. var text = dataLabel.text;
  383. if (text) {
  384. css(text.element.parentNode, {
  385. width: width + 'px',
  386. height: height + 'px'
  387. });
  388. // Set properties for the span emulating `text`
  389. css(text.element, {
  390. left: 0,
  391. top: 0,
  392. width: '100%',
  393. height: '100%',
  394. overflow: 'hidden'
  395. });
  396. }
  397. // The getBBox function is used in `alignDataLabel` to align
  398. // inside the box
  399. dataLabel.getBBox = function () {
  400. return {
  401. width: width,
  402. height: height
  403. };
  404. };
  405. // Overwrite dataLabel dimensions (#13100).
  406. dataLabel.width = width;
  407. dataLabel.height = height;
  408. }
  409. H.seriesTypes.column.prototype.alignDataLabel.apply(this, arguments);
  410. }
  411. });
  412. /**
  413. * An `organization` series. If the [type](#series.organization.type) option is
  414. * not specified, it is inherited from [chart.type](#chart.type).
  415. *
  416. * @extends series,plotOptions.organization
  417. * @exclude dataSorting, boostThreshold, boostBlending
  418. * @product highcharts
  419. * @requires modules/organization
  420. * @apioption series.organization
  421. */
  422. /**
  423. * @type {Highcharts.SeriesOrganizationDataLabelsOptionsObject|Array<Highcharts.SeriesOrganizationDataLabelsOptionsObject>}
  424. * @product highcharts
  425. * @apioption series.organization.data.dataLabels
  426. */
  427. /**
  428. * A collection of options for the individual nodes. The nodes in an org chart
  429. * are auto-generated instances of `Highcharts.Point`, but options can be
  430. * applied here and linked by the `id`.
  431. *
  432. * @extends series.sankey.nodes
  433. * @type {Array<*>}
  434. * @product highcharts
  435. * @apioption series.organization.nodes
  436. */
  437. /**
  438. * Individual data label for each node. The options are the same as
  439. * the ones for [series.organization.dataLabels](#series.organization.dataLabels).
  440. *
  441. * @type {Highcharts.SeriesOrganizationDataLabelsOptionsObject|Array<Highcharts.SeriesOrganizationDataLabelsOptionsObject>}
  442. *
  443. * @apioption series.organization.nodes.dataLabels
  444. */
  445. /**
  446. * The job description for the node card, will be inserted by the default
  447. * `dataLabel.nodeFormatter`.
  448. *
  449. * @sample highcharts/demo/organization-chart
  450. * Org chart with job descriptions
  451. *
  452. * @type {string}
  453. * @product highcharts
  454. * @apioption series.organization.nodes.description
  455. */
  456. /**
  457. * An image for the node card, will be inserted by the default
  458. * `dataLabel.nodeFormatter`.
  459. *
  460. * @sample highcharts/demo/organization-chart
  461. * Org chart with images
  462. *
  463. * @type {string}
  464. * @product highcharts
  465. * @apioption series.organization.nodes.image
  466. */
  467. /**
  468. * Layout for the node's children. If `hanging`, this node's children will hang
  469. * below their parent, allowing a tighter packing of nodes in the diagram.
  470. *
  471. * @sample highcharts/demo/organization-chart
  472. * Hanging layout
  473. *
  474. * @type {Highcharts.SeriesOrganizationNodesLayoutValue}
  475. * @default normal
  476. * @product highcharts
  477. * @apioption series.organization.nodes.layout
  478. */
  479. /**
  480. * The job title for the node card, will be inserted by the default
  481. * `dataLabel.nodeFormatter`.
  482. *
  483. * @sample highcharts/demo/organization-chart
  484. * Org chart with job titles
  485. *
  486. * @type {string}
  487. * @product highcharts
  488. * @apioption series.organization.nodes.title
  489. */
  490. /**
  491. * An array of data points for the series. For the `organization` series
  492. * type, points can be given in the following way:
  493. *
  494. * An array of objects with named values. The following snippet shows only a
  495. * few settings, see the complete options set below. If the total number of data
  496. * points exceeds the series' [turboThreshold](#series.area.turboThreshold),
  497. * this option is not available.
  498. *
  499. * ```js
  500. * data: [{
  501. * from: 'Category1',
  502. * to: 'Category2',
  503. * weight: 2
  504. * }, {
  505. * from: 'Category1',
  506. * to: 'Category3',
  507. * weight: 5
  508. * }]
  509. * ```
  510. *
  511. * @type {Array<*>}
  512. * @extends series.sankey.data
  513. * @product highcharts
  514. * @apioption series.organization.data
  515. */
  516. ''; // adds doclets above to transpiled file