/* *
*
* 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