/* * * * Organization chart module * * (c) 2018-2020 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import H from '../parts/Globals.js'; import U from '../parts/Utilities.js'; var css = U.css, pick = U.pick, seriesType = U.seriesType, wrap = U.wrap; /** * Layout value for the child nodes in an organization chart. If `hanging`, this * node's children will hang below their parent, allowing a tighter packing of * nodes in the diagram. * * @typedef {"normal"|"hanging"} Highcharts.SeriesOrganizationNodesLayoutValue */ var base = H.seriesTypes.sankey.prototype; /** * @private * @class * @name Highcharts.seriesTypes.organization * * @augments Highcharts.seriesTypes.sankey */ seriesType('organization', 'sankey', /** * An organization chart is a diagram that shows the structure of an * organization and the relationships and relative ranks of its parts and * positions. * * @sample highcharts/demo/organization-chart/ * Organization chart * @sample highcharts/series-organization/horizontal/ * Horizontal organization chart * @sample highcharts/series-organization/borderless * Borderless design * @sample highcharts/series-organization/center-layout * Centered layout * * @extends plotOptions.sankey * @excluding allowPointSelect, curveFactor, dataSorting * @since 7.1.0 * @product highcharts * @requires modules/organization * @optionparent plotOptions.organization */ { /** * The border color of the node cards. * * @type {Highcharts.ColorString} * @private */ borderColor: '#666666', /** * The border radius of the node cards. * * @private */ borderRadius: 3, /** * Radius for the rounded corners of the links between nodes. * * @sample highcharts/series-organization/link-options * Square links * * @private */ linkRadius: 10, borderWidth: 1, /** * @declare Highcharts.SeriesOrganizationDataLabelsOptionsObject * * @private */ dataLabels: { /* eslint-disable valid-jsdoc */ /** * A callback for defining the format for _nodes_ in the * organization chart. The `nodeFormat` option takes precedence * over `nodeFormatter`. * * In an organization chart, the `nodeFormatter` is a quite complex * function of the available options, striving for a good default * layout of cards with or without images. In organization chart, * the data labels come with `useHTML` set to true, meaning they * will be rendered as true HTML above the SVG. * * @sample highcharts/series-organization/datalabels-nodeformatter * Modify the default label format output * * @type {Highcharts.SeriesSankeyDataLabelsFormatterCallbackFunction} * @since 6.0.2 */ nodeFormatter: function () { var outerStyle = { width: '100%', height: '100%', display: 'flex', 'flex-direction': 'row', 'align-items': 'center', 'justify-content': 'center' }, imageStyle = { 'max-height': '100%', 'border-radius': '50%' }, innerStyle = { width: '100%', padding: 0, 'text-align': 'center', 'white-space': 'normal' }, nameStyle = { margin: 0 }, titleStyle = { margin: 0 }, descriptionStyle = { opacity: 0.75, margin: '5px' }; // eslint-disable-next-line valid-jsdoc /** * @private */ function styleAttr(style) { return Object.keys(style).reduce(function (str, key) { return str + key + ':' + style[key] + ';'; }, 'style="') + '"'; } if (this.point.image) { imageStyle['max-width'] = '30%'; innerStyle.width = '70%'; } // PhantomJS doesn't support flex, roll back to absolute // positioning if (this.series.chart.renderer.forExport) { outerStyle.display = 'block'; innerStyle.position = 'absolute'; innerStyle.left = this.point.image ? '30%' : 0; innerStyle.top = 0; } var html = '
'; if (this.point.image) { html += ''; } html += '
'; if (this.point.name) { html += '

' + this.point.name + '

'; } if (this.point.title) { html += '

' + (this.point.title || '') + '

'; } if (this.point.description) { html += '

' + this.point.description + '

'; } html += '
' + '
'; return html; }, /* eslint-enable valid-jsdoc */ style: { /** @internal */ fontWeight: 'normal', /** @internal */ fontSize: '13px' }, useHTML: true }, /** * The indentation in pixels of hanging nodes, nodes which parent has * [layout](#series.organization.nodes.layout) set to `hanging`. * * @private */ hangingIndent: 20, /** * The color of the links between nodes. * * @type {Highcharts.ColorString} * @private */ linkColor: '#666666', /** * The line width of the links connecting nodes, in pixels. * * @sample highcharts/series-organization/link-options * Square links * * @private */ linkLineWidth: 1, /** * In a horizontal chart, the width of the nodes in pixels. Node that * most organization charts are vertical, so the name of this option * is counterintuitive. * * @private */ nodeWidth: 50, tooltip: { nodeFormat: '{point.name}
{point.title}
{point.description}' } }, { pointAttribs: function (point, state) { 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'] .reduce(function (obj, key) { obj[key] = pick(stateOptions[key], options[key], levelOptions[key], series.options[key]); return obj; }, {}); if (!point.isNode) { attribs.stroke = values.linkColor; attribs['stroke-width'] = values.linkLineWidth; delete attribs.fill; } else { if (values.borderRadius) { attribs.r = values.borderRadius; } } return attribs; }, createNode: function (id) { var node = base.createNode .call(this, id); // All nodes in an org chart are equal width node.getSum = function () { return 1; }; return node; }, createNodeColumn: function () { var column = base.createNodeColumn.call(this); // Wrap the offset function so that the hanging node's children are // aligned to their parent wrap(column, 'offset', function (proceed, node, factor) { var offset = proceed.call(this, node, factor); // eslint-disable-line no-invalid-this // Modify the default output if the parent's layout is 'hanging' if (node.hangsFrom) { return { absoluteTop: node.hangsFrom.nodeY }; } return offset; }); return column; }, translateNode: function (node, column) { base.translateNode.call(this, node, column); if (node.hangsFrom) { node.shapeArgs.height -= this.options.hangingIndent; if (!this.chart.inverted) { node.shapeArgs.y += this.options.hangingIndent; } } node.nodeHeight = this.chart.inverted ? node.shapeArgs.width : node.shapeArgs.height; }, // General function to apply corner radius to a path - can be lifted to // renderer or utilities if we need it elsewhere. curvedPath: function (path, r) { var d = []; for (var i = 0; i < path.length; i++) { var x = path[i][1]; var y = path[i][2]; if (typeof x === 'number' && typeof y === 'number') { // moveTo if (i === 0) { d.push(['M', x, y]); } else if (i === path.length - 1) { d.push(['L', x, y]); // curveTo } else if (r) { var prevSeg = path[i - 1]; var nextSeg = path[i + 1]; if (prevSeg && nextSeg) { var x1 = prevSeg[1], y1 = prevSeg[2], x2 = nextSeg[1], y2 = nextSeg[2]; // Only apply to breaks if (typeof x1 === 'number' && typeof x2 === 'number' && typeof y1 === 'number' && typeof y2 === 'number' && x1 !== x2 && y1 !== y2) { var directionX = x1 < x2 ? 1 : -1, directionY = y1 < y2 ? 1 : -1; d.push([ 'L', x - directionX * Math.min(Math.abs(x - x1), r), y - directionY * Math.min(Math.abs(y - y1), r) ], [ 'C', x, y, x, y, x + directionX * Math.min(Math.abs(x - x2), r), y + directionY * Math.min(Math.abs(y - y2), r) ]); } } // lineTo } else { d.push(['L', x, y]); } } } return d; }, translateLink: function (point) { var fromNode = point.fromNode, toNode = point.toNode, crisp = Math.round(this.options.linkLineWidth) % 2 / 2, x1 = Math.floor(fromNode.shapeArgs.x + fromNode.shapeArgs.width) + crisp, y1 = Math.floor(fromNode.shapeArgs.y + fromNode.shapeArgs.height / 2) + crisp, x2 = Math.floor(toNode.shapeArgs.x) + crisp, y2 = Math.floor(toNode.shapeArgs.y + toNode.shapeArgs.height / 2) + crisp, xMiddle, hangingIndent = this.options.hangingIndent, toOffset = toNode.options.offset, percentOffset = /%$/.test(toOffset) && parseInt(toOffset, 10), inverted = this.chart.inverted; if (inverted) { x1 -= fromNode.shapeArgs.width; x2 += toNode.shapeArgs.width; } xMiddle = Math.floor(x2 + (inverted ? 1 : -1) * (this.colDistance - this.nodeWidth) / 2) + crisp; // Put the link on the side of the node when an offset is given. HR // node in the main demo. if (percentOffset && (percentOffset >= 50 || percentOffset <= -50)) { xMiddle = x2 = Math.floor(x2 + (inverted ? -0.5 : 0.5) * toNode.shapeArgs.width) + crisp; y2 = toNode.shapeArgs.y; if (percentOffset > 0) { y2 += toNode.shapeArgs.height; } } if (toNode.hangsFrom === fromNode) { if (this.chart.inverted) { y1 = Math.floor(fromNode.shapeArgs.y + fromNode.shapeArgs.height - hangingIndent / 2) + crisp; y2 = (toNode.shapeArgs.y + toNode.shapeArgs.height); } else { y1 = Math.floor(fromNode.shapeArgs.y + hangingIndent / 2) + crisp; } xMiddle = x2 = Math.floor(toNode.shapeArgs.x + toNode.shapeArgs.width / 2) + crisp; } point.plotY = 1; point.shapeType = 'path'; point.shapeArgs = { d: this.curvedPath([ ['M', x1, y1], ['L', xMiddle, y1], ['L', xMiddle, y2], ['L', x2, y2] ], this.options.linkRadius) }; }, alignDataLabel: function (point, dataLabel, options) { // Align the data label to the point graphic if (options.useHTML) { var width = point.shapeArgs.width, height = point.shapeArgs.height, padjust = (this.options.borderWidth + 2 * this.options.dataLabels.padding); if (this.chart.inverted) { width = height; height = point.shapeArgs.width; } height -= padjust; width -= padjust; // Set the size of the surrounding div emulating `g` var text = dataLabel.text; if (text) { css(text.element.parentNode, { width: width + 'px', height: height + 'px' }); // Set properties for the span emulating `text` css(text.element, { left: 0, top: 0, width: '100%', height: '100%', overflow: 'hidden' }); } // The getBBox function is used in `alignDataLabel` to align // inside the box dataLabel.getBBox = function () { return { width: width, height: height }; }; // Overwrite dataLabel dimensions (#13100). dataLabel.width = width; dataLabel.height = height; } H.seriesTypes.column.prototype.alignDataLabel.apply(this, arguments); } }); /** * An `organization` series. If the [type](#series.organization.type) option is * not specified, it is inherited from [chart.type](#chart.type). * * @extends series,plotOptions.organization * @exclude dataSorting * @product highcharts * @requires modules/organization * @apioption series.organization */ /** * @type {Highcharts.SeriesOrganizationDataLabelsOptionsObject|Array} * @product highcharts * @apioption series.organization.data.dataLabels */ /** * A collection of options for the individual nodes. The nodes in an org chart * are auto-generated instances of `Highcharts.Point`, but options can be * applied here and linked by the `id`. * * @extends series.sankey.nodes * @type {Array<*>} * @product highcharts * @apioption series.organization.nodes */ /** * Individual data label for each node. The options are the same as * the ones for [series.organization.dataLabels](#series.organization.dataLabels). * * @type {Highcharts.SeriesOrganizationDataLabelsOptionsObject|Array} * * @apioption series.organization.nodes.dataLabels */ /** * The job description for the node card, will be inserted by the default * `dataLabel.nodeFormatter`. * * @sample highcharts/demo/organization-chart * Org chart with job descriptions * * @type {string} * @product highcharts * @apioption series.organization.nodes.description */ /** * An image for the node card, will be inserted by the default * `dataLabel.nodeFormatter`. * * @sample highcharts/demo/organization-chart * Org chart with images * * @type {string} * @product highcharts * @apioption series.organization.nodes.image */ /** * Layout for the node's children. If `hanging`, this node's children will hang * below their parent, allowing a tighter packing of nodes in the diagram. * * @sample highcharts/demo/organization-chart * Hanging layout * * @type {Highcharts.SeriesOrganizationNodesLayoutValue} * @default normal * @product highcharts * @apioption series.organization.nodes.layout */ /** * The job title for the node card, will be inserted by the default * `dataLabel.nodeFormatter`. * * @sample highcharts/demo/organization-chart * Org chart with job titles * * @type {string} * @product highcharts * @apioption series.organization.nodes.title */ /** * An array of data points for the series. For the `organization` series * type, points can be given in the following way: * * An array of objects with named values. The following snippet shows only a * few settings, see the complete options set below. If the total number of data * points exceeds the series' [turboThreshold](#series.area.turboThreshold), * this option is not available. * * ```js * data: [{ * from: 'Category1', * to: 'Category2', * weight: 2 * }, { * from: 'Category1', * to: 'Category3', * weight: 5 * }] * ``` * * @type {Array<*>} * @extends series.sankey.data * @product highcharts * @apioption series.organization.data */ ''; // adds doclets above to transpiled file