highcharts
Version:
JavaScript charting framework
288 lines (287 loc) • 12.8 kB
JavaScript
/* *
*
* Organization chart module
*
* (c) 2018-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
;
import OrganizationPoint from './OrganizationPoint.js';
import OrganizationSeriesDefaults from './OrganizationSeriesDefaults.js';
import SeriesRegistry from '../../Core/Series/SeriesRegistry.js';
import PathUtilities from '../PathUtilities.js';
const { sankey: SankeySeries } = SeriesRegistry.seriesTypes;
import U from '../../Core/Utilities.js';
const { css, crisp, extend, isNumber, merge, pick } = U;
import SVGElement from '../../Core/Renderer/SVG/SVGElement.js';
import TextPath from '../../Extensions/TextPath.js';
TextPath.compose(SVGElement);
/* *
*
* Class
*
* */
/**
* @private
* @class
* @name Highcharts.seriesTypes.organization
*
* @augments Highcharts.seriesTypes.sankey
*/
class OrganizationSeries extends SankeySeries {
/* *
*
* Functions
*
* */
alignDataLabel(point, dataLabel, options) {
// Align the data label to the point graphic
const shapeArgs = point.shapeArgs, text = dataLabel.text;
if (options.useHTML && shapeArgs) {
const padjust = (this.options.borderWidth +
2 * this.options.dataLabels.padding);
let width = shapeArgs.width || 0, height = shapeArgs.height || 0;
if (this.chart.inverted) {
width = height;
height = shapeArgs.width || 0;
}
height -= padjust;
width -= padjust;
text.foreignObject?.attr({
x: 0,
y: 0,
width,
height
});
// When foreign object, the parent node is the body. When parallel
// HTML, it is the surrounding div emulating `g`
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 = () => ({ width, height, x: 0, y: 0 });
// Overwrite dataLabel dimensions (#13100).
dataLabel.width = width;
dataLabel.height = height;
}
super.alignDataLabel.apply(this, arguments);
}
createNode(id) {
const node = super.createNode.call(this, id);
// All nodes in an org chart are equal width
node.getSum = () => 1;
return node;
}
pointAttribs(point, state) {
const series = this, attribs = SankeySeries.prototype.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]) ||
{}, borderRadius = pick(stateOptions.borderRadius, options.borderRadius, levelOptions.borderRadius, series.options.borderRadius), linkColor = pick(stateOptions.linkColor, options.linkColor, levelOptions.linkColor, series.options.linkColor, stateOptions.link && stateOptions.link.color, options.link && options.link.color, levelOptions.link && levelOptions.link.color, series.options.link && series.options.link.color), linkLineWidth = pick(stateOptions.linkLineWidth, options.linkLineWidth, levelOptions.linkLineWidth, series.options.linkLineWidth, stateOptions.link && stateOptions.link.lineWidth, options.link && options.link.lineWidth, levelOptions.link && levelOptions.link.lineWidth, series.options.link && series.options.link.lineWidth), linkOpacity = pick(stateOptions.linkOpacity, options.linkOpacity, levelOptions.linkOpacity, series.options.linkOpacity, stateOptions.link && stateOptions.link.linkOpacity, options.link && options.link.linkOpacity, levelOptions.link && levelOptions.link.linkOpacity, series.options.link && series.options.link.linkOpacity);
if (!point.isNode) {
attribs.stroke = linkColor;
attribs['stroke-width'] = linkLineWidth;
attribs.opacity = linkOpacity;
delete attribs.fill;
}
else {
if (isNumber(borderRadius)) {
attribs.r = borderRadius;
}
}
return attribs;
}
translateLink(point) {
const chart = this.chart, options = this.options, fromNode = point.fromNode, toNode = point.toNode, linkWidth = pick(options.linkLineWidth, options.link.lineWidth, 0), factor = pick(options.link.offset, 0.5), type = pick(point.options.link && point.options.link.type, options.link.type);
if (fromNode.shapeArgs && toNode.shapeArgs) {
const hangingIndent = options.hangingIndent, hangingRight = options.hangingSide === 'right', toOffset = toNode.options.offset, percentOffset = /%$/.test(toOffset) && parseInt(toOffset, 10), inverted = chart.inverted;
let x1 = crisp((fromNode.shapeArgs.x || 0) +
(fromNode.shapeArgs.width || 0), linkWidth), y1 = crisp((fromNode.shapeArgs.y || 0) +
(fromNode.shapeArgs.height || 0) / 2, linkWidth), x2 = crisp(toNode.shapeArgs.x || 0, linkWidth), y2 = crisp((toNode.shapeArgs.y || 0) +
(toNode.shapeArgs.height || 0) / 2, linkWidth), xMiddle;
if (inverted) {
x1 -= (fromNode.shapeArgs.width || 0);
x2 += (toNode.shapeArgs.width || 0);
}
xMiddle = this.colDistance ?
crisp(x2 +
((inverted ? 1 : -1) *
(this.colDistance - this.nodeWidth)) /
2, linkWidth) :
crisp((x2 + x1) / 2, linkWidth);
// 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 = crisp(x2 + (inverted ? -0.5 : 0.5) *
(toNode.shapeArgs.width || 0), linkWidth);
y2 = toNode.shapeArgs.y || 0;
if (percentOffset > 0) {
y2 += toNode.shapeArgs.height || 0;
}
}
if (toNode.hangsFrom === fromNode) {
if (chart.inverted) {
y1 = !hangingRight ?
crisp((fromNode.shapeArgs.y || 0) +
(fromNode.shapeArgs.height || 0) -
hangingIndent / 2, linkWidth) :
crisp((fromNode.shapeArgs.y || 0) + hangingIndent / 2, linkWidth);
y2 = !hangingRight ? ((toNode.shapeArgs.y || 0) +
(toNode.shapeArgs.height || 0)) : (toNode.shapeArgs.y || 0) + hangingIndent / 2;
}
else {
y1 = crisp((fromNode.shapeArgs.y || 0) + hangingIndent / 2, linkWidth);
}
xMiddle = x2 = crisp((toNode.shapeArgs.x || 0) +
(toNode.shapeArgs.width || 0) / 2, linkWidth);
}
point.plotX = xMiddle;
point.plotY = (y1 + y2) / 2;
point.shapeType = 'path';
if (type === 'straight') {
point.shapeArgs = {
d: [
['M', x1, y1],
['L', x2, y2]
]
};
}
else if (type === 'curved') {
const offset = Math.abs(x2 - x1) * factor * (inverted ? -1 : 1);
point.shapeArgs = {
d: [
['M', x1, y1],
['C', x1 + offset, y1, x2 - offset, y2, x2, y2]
]
};
}
else {
point.shapeArgs = {
d: PathUtilities.applyRadius([
['M', x1, y1],
['L', xMiddle, y1],
['L', xMiddle, y2],
['L', x2, y2]
], pick(options.linkRadius, options.link.radius))
};
}
point.dlBox = {
x: (x1 + x2) / 2,
y: (y1 + y2) / 2,
height: linkWidth,
width: 0
};
}
}
translateNode(node, column) {
super.translateNode(node, column);
const chart = this.chart, options = this.options, sum = node.getSum(), translationFactor = this.translationFactor, nodeHeight = Math.max(Math.round(sum * translationFactor), options.minLinkWidth || 0), hangingRight = options.hangingSide === 'right', indent = options.hangingIndent || 0, indentLogic = options.hangingIndentTranslation, minLength = options.minNodeLength || 10, nodeWidth = Math.round(this.nodeWidth), shapeArgs = node.shapeArgs, sign = chart.inverted ? -1 : 1;
let parentNode = node.hangsFrom;
if (parentNode) {
if (indentLogic === 'cumulative') {
// Move to the right:
shapeArgs.height -= indent;
// If hanging right, first indent is handled by shrinking.
if (chart.inverted && !hangingRight) {
shapeArgs.y -= sign * indent;
}
while (parentNode) {
// Hanging right is the same direction as non-inverted.
shapeArgs.y += (hangingRight ? 1 : sign) * indent;
parentNode = parentNode.hangsFrom;
}
}
else if (indentLogic === 'shrink') {
// Resize the node:
while (parentNode &&
shapeArgs.height > indent + minLength) {
shapeArgs.height -= indent;
// Fixes nodes not dropping in non-inverted charts.
// Hanging right is the same as non-inverted.
if (!chart.inverted || hangingRight) {
shapeArgs.y += indent;
}
parentNode = parentNode.hangsFrom;
}
}
else {
// Option indentLogic === "inherit"
// Do nothing (v9.3.2 and prev versions):
shapeArgs.height -= indent;
if (!chart.inverted || hangingRight) {
shapeArgs.y += indent;
}
}
}
node.nodeHeight = chart.inverted ?
shapeArgs.width :
shapeArgs.height;
// Calculate shape args correctly to align nodes to center (#19946)
if (node.shapeArgs && !node.hangsFrom) {
node.shapeArgs = merge(node.shapeArgs, {
x: (node.shapeArgs.x || 0) + (nodeWidth / 2) -
((node.shapeArgs.width || 0) / 2),
y: (node.shapeArgs.y || 0) + (nodeHeight / 2) -
((node.shapeArgs.height || 0) / 2)
});
}
}
drawDataLabels() {
const dlOptions = this.options.dataLabels;
if (dlOptions.linkTextPath && dlOptions.linkTextPath.enabled) {
for (const link of this.points) {
link.options.dataLabels = merge(link.options.dataLabels, { useHTML: false });
}
}
super.drawDataLabels();
}
}
/* *
*
* Static Properties
*
* */
OrganizationSeries.defaultOptions = merge(SankeySeries.defaultOptions, OrganizationSeriesDefaults);
extend(OrganizationSeries.prototype, {
pointClass: OrganizationPoint
});
SeriesRegistry.registerSeriesType('organization', OrganizationSeries);
/* *
*
* Default Export
*
* */
export default OrganizationSeries;
/* *
*
* API Declarations
*
* */
/**
* 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
*/
/**
* Indent translation value for the child nodes in an organization chart, when
* parent has `hanging` layout. Option can shrink nodes (for tight charts),
* translate children to the left, or render nodes directly under the parent.
*
* @typedef {"inherit"|"cumulative"|"shrink"} Highcharts.OrganizationHangingIndentTranslationValue
*/
''; // Detach doclets above