1744 lines
73 KiB
JavaScript
1744 lines
73 KiB
JavaScript
/**
|
|
* @license Highcharts JS v11.4.1 (2024-04-04)
|
|
* Treegraph chart series type
|
|
*
|
|
* (c) 2010-2024 Pawel Lysy Grzegorz Blachlinski
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*/
|
|
(function (factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
factory['default'] = factory;
|
|
module.exports = factory;
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
define('highcharts/modules/treegraph', ['highcharts', 'highcharts/modules/treemap'], function (Highcharts) {
|
|
factory(Highcharts);
|
|
factory.Highcharts = Highcharts;
|
|
return factory;
|
|
});
|
|
} else {
|
|
factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined);
|
|
}
|
|
}(function (Highcharts) {
|
|
'use strict';
|
|
var _modules = Highcharts ? Highcharts._modules : {};
|
|
function _registerModule(obj, path, args, fn) {
|
|
if (!obj.hasOwnProperty(path)) {
|
|
obj[path] = fn.apply(null, args);
|
|
|
|
if (typeof CustomEvent === 'function') {
|
|
window.dispatchEvent(new CustomEvent(
|
|
'HighchartsModuleLoaded',
|
|
{ detail: { path: path, module: obj[path] } }
|
|
));
|
|
}
|
|
}
|
|
}
|
|
_registerModule(_modules, 'Series/PathUtilities.js', [], function () {
|
|
/* *
|
|
*
|
|
* (c) 2010-2024 Pawel Lysy
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const getLinkPath = {
|
|
'default': getDefaultPath,
|
|
straight: getStraightPath,
|
|
curved: getCurvedPath
|
|
};
|
|
/**
|
|
*
|
|
*/
|
|
function getDefaultPath(pathParams) {
|
|
const { x1, y1, x2, y2, width = 0, inverted = false, radius, parentVisible } = pathParams;
|
|
const path = [
|
|
['M', x1, y1],
|
|
['L', x1, y1],
|
|
['C', x1, y1, x1, y2, x1, y2],
|
|
['L', x1, y2],
|
|
['C', x1, y1, x1, y2, x1, y2],
|
|
['L', x1, y2]
|
|
];
|
|
return parentVisible ?
|
|
applyRadius([
|
|
['M', x1, y1],
|
|
['L', x1 + width * (inverted ? -0.5 : 0.5), y1],
|
|
['L', x1 + width * (inverted ? -0.5 : 0.5), y2],
|
|
['L', x2, y2]
|
|
], radius) :
|
|
path;
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
function getStraightPath(pathParams) {
|
|
const { x1, y1, x2, y2, width = 0, inverted = false, parentVisible } = pathParams;
|
|
return parentVisible ? [
|
|
['M', x1, y1],
|
|
['L', x1 + width * (inverted ? -1 : 1), y2],
|
|
['L', x2, y2]
|
|
] : [
|
|
['M', x1, y1],
|
|
['L', x1, y2],
|
|
['L', x1, y2]
|
|
];
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
function getCurvedPath(pathParams) {
|
|
const { x1, y1, x2, y2, offset = 0, width = 0, inverted = false, parentVisible } = pathParams;
|
|
return parentVisible ?
|
|
[
|
|
['M', x1, y1],
|
|
[
|
|
'C',
|
|
x1 + offset,
|
|
y1,
|
|
x1 - offset + width * (inverted ? -1 : 1),
|
|
y2,
|
|
x1 + width * (inverted ? -1 : 1),
|
|
y2
|
|
],
|
|
['L', x2, y2]
|
|
] :
|
|
[
|
|
['M', x1, y1],
|
|
['C', x1, y1, x1, y2, x1, y2],
|
|
['L', x2, y2]
|
|
];
|
|
}
|
|
/**
|
|
* General function to apply corner radius to a path
|
|
* @private
|
|
*/
|
|
function applyRadius(path, r) {
|
|
const d = [];
|
|
for (let i = 0; i < path.length; i++) {
|
|
const x = path[i][1];
|
|
const 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) {
|
|
const prevSeg = path[i - 1];
|
|
const nextSeg = path[i + 1];
|
|
if (prevSeg && nextSeg) {
|
|
const 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) {
|
|
const 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;
|
|
}
|
|
const PathUtilities = {
|
|
applyRadius,
|
|
getLinkPath
|
|
};
|
|
|
|
return PathUtilities;
|
|
});
|
|
_registerModule(_modules, 'Series/Treegraph/TreegraphNode.js', [_modules['Core/Series/SeriesRegistry.js']], function (SeriesRegistry) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2024 Pawel Lysy Grzegorz Blachlinski
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { seriesTypes: { treemap: { prototype: { NodeClass: TreemapNode } } } } = SeriesRegistry;
|
|
/* *
|
|
*
|
|
* Class
|
|
*
|
|
* */
|
|
/**
|
|
* @private
|
|
* @class
|
|
*/
|
|
class TreegraphNode extends TreemapNode {
|
|
constructor() {
|
|
/* *
|
|
*
|
|
* Properties
|
|
*
|
|
* */
|
|
super(...arguments);
|
|
this.mod = 0;
|
|
this.shift = 0;
|
|
this.change = 0;
|
|
this.children = [];
|
|
this.preX = 0;
|
|
this.hidden = false;
|
|
this.wasVisited = false;
|
|
this.collapsed = false;
|
|
}
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
/**
|
|
* Get the next left node which is either first child or thread.
|
|
*
|
|
* @return {TreegraphNode|undefined}
|
|
* Next left node child or thread.
|
|
*/
|
|
nextLeft() {
|
|
return this.getLeftMostChild() || this.thread;
|
|
}
|
|
/**
|
|
* Get the next right node which is either last child or thread.
|
|
*
|
|
* @return {TreegraphNode|undefined}
|
|
* Next right node child or thread.
|
|
*/
|
|
nextRight() {
|
|
return this.getRightMostChild() || this.thread;
|
|
}
|
|
/**
|
|
* Return the left one of the greatest uncommon ancestors of a
|
|
* leftInternal node and it's right neighbor.
|
|
*
|
|
* @param {TreegraphNode} leftIntNode
|
|
* @param {TreegraphNode} defaultAncestor
|
|
* @return {TreegraphNode}
|
|
* Left one of the greatest uncommon ancestors of a leftInternal
|
|
* node and it's right neighbor.
|
|
*
|
|
*/
|
|
getAncestor(leftIntNode, defaultAncestor) {
|
|
const leftAnc = leftIntNode.ancestor;
|
|
if (leftAnc.children[0] === this.children[0]) {
|
|
return leftIntNode.ancestor;
|
|
}
|
|
return defaultAncestor;
|
|
}
|
|
/**
|
|
* Get node's first sibling, which is not hidden.
|
|
*
|
|
* @return {TreegraphNode|undefined}
|
|
* First sibling of the node which is not hidden or undefined, if it
|
|
* does not exists.
|
|
*/
|
|
getLeftMostSibling() {
|
|
const parent = this.getParent();
|
|
if (parent) {
|
|
for (const child of parent.children) {
|
|
if (child && child.point.visible) {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Check if the node is a leaf (if it has any children).
|
|
*
|
|
* @return {boolean}
|
|
* If the node has no visible children return true.
|
|
*/
|
|
hasChildren() {
|
|
const children = this.children;
|
|
for (let i = 0; i < children.length; i++) {
|
|
if (children[i].point.visible) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Get node's left sibling (if it exists).
|
|
*
|
|
* @return {TreegraphNode|undefined}
|
|
* Left sibling of the node
|
|
*/
|
|
getLeftSibling() {
|
|
const parent = this.getParent();
|
|
if (parent) {
|
|
const children = parent.children;
|
|
for (let i = this.relativeXPosition - 1; i >= 0; i--) {
|
|
if (children[i] && children[i].point.visible) {
|
|
return children[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Get the node's first child (if it exists).
|
|
*
|
|
* @return {TreegraphNode|undefined}
|
|
* Node's first child which isn't hidden.
|
|
*/
|
|
getLeftMostChild() {
|
|
const children = this.children;
|
|
for (let i = 0; i < children.length; i++) {
|
|
if (children[i].point.visible) {
|
|
return children[i];
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Get the node's last child (if it exists).
|
|
*
|
|
* @return {TreegraphNode|undefined}
|
|
* Node's last child which isn't hidden.
|
|
*/
|
|
getRightMostChild() {
|
|
const children = this.children;
|
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
if (children[i].point.visible) {
|
|
return children[i];
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Get the parent of current node or return undefined for root of the
|
|
* tree.
|
|
*
|
|
* @return {TreegraphNode|undefined}
|
|
* Node's parent or undefined for root.
|
|
*/
|
|
getParent() {
|
|
return this.parentNode;
|
|
}
|
|
/**
|
|
* Get node's first child which is not hidden.
|
|
*
|
|
* @return {TreegraphNode|undefined}
|
|
* First child.
|
|
*/
|
|
getFirstChild() {
|
|
const children = this.children;
|
|
for (let i = 0; i < children.length; i++) {
|
|
if (children[i].point.visible) {
|
|
return children[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return TreegraphNode;
|
|
});
|
|
_registerModule(_modules, 'Series/Treegraph/TreegraphPoint.js', [_modules['Core/Series/Point.js'], _modules['Core/Series/SeriesRegistry.js'], _modules['Core/Utilities.js']], function (Point, SeriesRegistry, U) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2024 Pawel Lysy Grzegorz Blachlinski
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { seriesTypes: { treemap: { prototype: { pointClass: TreemapPoint } } } } = SeriesRegistry;
|
|
const { addEvent, fireEvent, merge } = U;
|
|
/* *
|
|
*
|
|
* Class
|
|
*
|
|
* */
|
|
/**
|
|
* @private
|
|
* @class
|
|
*/
|
|
class TreegraphPoint extends TreemapPoint {
|
|
constructor() {
|
|
/* *
|
|
*
|
|
* Properties
|
|
*
|
|
* */
|
|
super(...arguments);
|
|
this.dataLabelOnHidden = true;
|
|
this.isLink = false;
|
|
this.setState = Point.prototype.setState;
|
|
}
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
draw() {
|
|
super.draw.apply(this, arguments);
|
|
// Run animation of hiding/showing of the point.
|
|
const graphic = this.graphic;
|
|
if (graphic) {
|
|
graphic.animate({
|
|
visibility: this.visible ? 'inherit' : 'hidden'
|
|
});
|
|
}
|
|
this.renderCollapseButton();
|
|
}
|
|
renderCollapseButton() {
|
|
const point = this, series = point.series, parentGroup = point.graphic && point.graphic.parentGroup, levelOptions = series.mapOptionsToLevel[point.node.level || 0] || {}, btnOptions = merge(series.options.collapseButton, levelOptions.collapseButton, point.options.collapseButton), { width, height, shape, style } = btnOptions, padding = 2, chart = this.series.chart, calculatedOpacity = (point.visible &&
|
|
(point.collapsed ||
|
|
!btnOptions.onlyOnHover ||
|
|
point.state === 'hover')) ? 1 : 0;
|
|
if (!point.shapeArgs) {
|
|
return;
|
|
}
|
|
this.collapseButtonOptions = btnOptions;
|
|
if (!point.collapseButton) {
|
|
if (!point.node.children.length || !btnOptions.enabled) {
|
|
return;
|
|
}
|
|
const { x, y } = this.getCollapseBtnPosition(btnOptions), fill = (btnOptions.fillColor ||
|
|
point.color ||
|
|
"#cccccc" /* Palette.neutralColor20 */);
|
|
point.collapseButton = chart.renderer
|
|
.label(point.collapsed ? '+' : '-', x, y, shape)
|
|
.attr({
|
|
height: height - 2 * padding,
|
|
width: width - 2 * padding,
|
|
padding: padding,
|
|
fill,
|
|
rotation: chart.inverted ? 90 : 0,
|
|
rotationOriginX: width / 2,
|
|
rotationOriginY: height / 2,
|
|
stroke: btnOptions.lineColor || "#ffffff" /* Palette.backgroundColor */,
|
|
'stroke-width': btnOptions.lineWidth,
|
|
'text-align': 'center',
|
|
align: 'center',
|
|
zIndex: 1,
|
|
opacity: calculatedOpacity,
|
|
visibility: point.visible ? 'inherit' : 'hidden'
|
|
})
|
|
.addClass('highcharts-tracker')
|
|
.addClass('highcharts-collapse-button')
|
|
.removeClass('highcharts-no-tooltip')
|
|
.css(merge({
|
|
color: typeof fill === 'string' ?
|
|
chart.renderer.getContrast(fill) :
|
|
"#333333" /* Palette.neutralColor80 */
|
|
}, style))
|
|
.add(parentGroup);
|
|
point.collapseButton.element.point = point;
|
|
}
|
|
else {
|
|
if (!point.node.children.length || !btnOptions.enabled) {
|
|
point.collapseButton.destroy();
|
|
delete point.collapseButton;
|
|
}
|
|
else {
|
|
const { x, y } = this.getCollapseBtnPosition(btnOptions);
|
|
point.collapseButton
|
|
.attr({
|
|
text: point.collapsed ? '+' : '-',
|
|
rotation: chart.inverted ? 90 : 0,
|
|
rotationOriginX: width / 2,
|
|
rotationOriginY: height / 2,
|
|
visibility: point.visible ? 'inherit' : 'hidden'
|
|
})
|
|
.animate({
|
|
x,
|
|
y,
|
|
opacity: calculatedOpacity
|
|
});
|
|
}
|
|
}
|
|
}
|
|
toggleCollapse(state) {
|
|
const series = this.series;
|
|
this.update({
|
|
collapsed: state ?? !this.collapsed
|
|
}, false, void 0, false);
|
|
fireEvent(series, 'toggleCollapse');
|
|
series.redraw();
|
|
}
|
|
destroy() {
|
|
if (this.collapseButton) {
|
|
this.collapseButton.destroy();
|
|
delete this.collapseButton;
|
|
this.collapseButton = void 0;
|
|
}
|
|
if (this.linkToParent) {
|
|
this.linkToParent.destroy();
|
|
delete this.linkToParent;
|
|
}
|
|
super.destroy.apply(this, arguments);
|
|
}
|
|
getCollapseBtnPosition(btnOptions) {
|
|
const point = this, chart = point.series.chart, inverted = chart.inverted, btnWidth = btnOptions.width, btnHeight = btnOptions.height, { x = 0, y = 0, width = 0, height = 0 } = point.shapeArgs || {};
|
|
return {
|
|
x: x +
|
|
btnOptions.x +
|
|
(inverted ? -btnHeight * 0.3 : width + btnWidth * -0.3),
|
|
y: y + height / 2 - btnHeight / 2 + btnOptions.y
|
|
};
|
|
}
|
|
}
|
|
addEvent(TreegraphPoint, 'mouseOut', function () {
|
|
const btn = this.collapseButton, btnOptions = this.collapseButtonOptions;
|
|
if (btn && btnOptions?.onlyOnHover && !this.collapsed) {
|
|
btn.animate({ opacity: 0 });
|
|
}
|
|
});
|
|
addEvent(TreegraphPoint, 'mouseOver', function () {
|
|
if (this.collapseButton && this.visible) {
|
|
this.collapseButton.animate({ opacity: 1 }, this.series.options.states?.hover?.animation);
|
|
}
|
|
});
|
|
// Handle showing and hiding of the points
|
|
addEvent(TreegraphPoint, 'click', function () {
|
|
this.toggleCollapse();
|
|
});
|
|
/* *
|
|
*
|
|
* Export Default
|
|
*
|
|
* */
|
|
|
|
return TreegraphPoint;
|
|
});
|
|
_registerModule(_modules, 'Series/Treegraph/TreegraphLink.js', [_modules['Core/Series/Point.js'], _modules['Core/Utilities.js'], _modules['Core/Series/SeriesRegistry.js']], function (Point, U, SeriesRegistry) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2024 Pawel Lysy Grzegorz Blachlinski
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { pick, extend } = U;
|
|
const { seriesTypes: { column: { prototype: { pointClass: ColumnPoint } } } } = SeriesRegistry;
|
|
/* *
|
|
*
|
|
* Class
|
|
*
|
|
* */
|
|
/**
|
|
* @private
|
|
* @class
|
|
*/
|
|
class LinkPoint extends ColumnPoint {
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
constructor(series, options, x, point) {
|
|
super(series, options, x);
|
|
/* *
|
|
*
|
|
* Class properties
|
|
*
|
|
* */
|
|
this.isLink = true;
|
|
this.node = {};
|
|
this.formatPrefix = 'link';
|
|
this.dataLabelOnNull = true;
|
|
this.formatPrefix = 'link';
|
|
this.dataLabelOnNull = true;
|
|
if (point) {
|
|
this.fromNode = point.node.parentNode.point;
|
|
this.visible = point.visible;
|
|
this.toNode = point;
|
|
this.id = this.toNode.id + '-' + this.fromNode.id;
|
|
}
|
|
}
|
|
update(options, redraw, animation, runEvent) {
|
|
const oldOptions = {
|
|
id: this.id,
|
|
formatPrefix: this.formatPrefix
|
|
};
|
|
Point.prototype.update.call(this, options, this.isLink ? false : redraw, // Hold the redraw for nodes
|
|
animation, runEvent);
|
|
this.visible = this.toNode.visible;
|
|
extend(this, oldOptions);
|
|
if (pick(redraw, true)) {
|
|
this.series.chart.redraw(animation);
|
|
}
|
|
}
|
|
}
|
|
/* *
|
|
*
|
|
* Export Default
|
|
*
|
|
* */
|
|
|
|
return LinkPoint;
|
|
});
|
|
_registerModule(_modules, 'Series/Treegraph/TreegraphLayout.js', [_modules['Series/Treegraph/TreegraphNode.js']], function (TreegraphNode) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2024 Pawel Lysy Grzegorz Blachlinski
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
/* *
|
|
*
|
|
* Class
|
|
*
|
|
* */
|
|
/**
|
|
* @private
|
|
* @class
|
|
*/
|
|
class TreegraphLayout {
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
/**
|
|
* Create dummy node, which allows to manually set the level of the node.
|
|
*
|
|
* @param {TreegraphNode} parent
|
|
* Parent node, to which the dummyNode should be connected.
|
|
* @param {TreegraphNode} child
|
|
* Child node, which should be connected to dummyNode.
|
|
* @param {number} gapSize
|
|
* Remaining gap size.
|
|
*
|
|
* @return {TreegraphNode}
|
|
* DummyNode as a parent of nodes, which column changes.
|
|
*/
|
|
static createDummyNode(parent, child, gapSize) {
|
|
// Initialise dummy node.
|
|
const dummyNode = new TreegraphNode();
|
|
dummyNode.id = parent.id + '-' + gapSize;
|
|
dummyNode.ancestor = parent;
|
|
// Add connection from new node to the previous points.
|
|
// First connection to itself.
|
|
dummyNode.children.push(child);
|
|
dummyNode.parent = parent.id;
|
|
dummyNode.parentNode = parent;
|
|
dummyNode.point = child.point;
|
|
dummyNode.level = child.level - gapSize;
|
|
dummyNode.relativeXPosition = child.relativeXPosition;
|
|
dummyNode.visible = child.visible;
|
|
// Then connection from parent to dummyNode.
|
|
parent.children[child.relativeXPosition] = dummyNode;
|
|
child.oldParentNode = parent;
|
|
child.relativeXPosition = 0;
|
|
// Then connection from child to dummyNode.
|
|
child.parentNode = dummyNode;
|
|
child.parent = dummyNode.id;
|
|
return dummyNode;
|
|
}
|
|
/**
|
|
* Walker algorithm of positioning the nodes in the treegraph improved by
|
|
* Buchheim to run in the linear time. Basic algorithm consists of post
|
|
* order traversal, which starts from going bottom up (first walk), and then
|
|
* pre order traversal top to bottom (second walk) where adding all of the
|
|
* modifiers is performed.
|
|
* link to the paper: http://dirk.jivas.de/papers/buchheim02improving.pdf
|
|
*
|
|
* @param {TreegraphSeries} series the Treegraph series
|
|
*/
|
|
calculatePositions(series) {
|
|
const treeLayout = this;
|
|
const nodes = series.nodeList;
|
|
this.resetValues(nodes);
|
|
const root = series.tree;
|
|
if (root) {
|
|
treeLayout.calculateRelativeX(root, 0);
|
|
treeLayout.beforeLayout(nodes);
|
|
treeLayout.firstWalk(root);
|
|
treeLayout.secondWalk(root, -root.preX);
|
|
treeLayout.afterLayout(nodes);
|
|
}
|
|
}
|
|
/**
|
|
* Create dummyNodes as parents for nodes, which column is changed.
|
|
*
|
|
* @param {Array<TreegraphNode>} nodes
|
|
* All of the nodes.
|
|
*/
|
|
beforeLayout(nodes) {
|
|
for (const node of nodes) {
|
|
for (let child of node.children) {
|
|
// Support for children placed in distant columns.
|
|
if (child && child.level - node.level > 1) {
|
|
// For further columns treat the nodes as a
|
|
// single parent-child pairs till the column is achieved.
|
|
let gapSize = child.level - node.level - 1;
|
|
// Parent -> dummyNode -> child
|
|
while (gapSize > 0) {
|
|
child = TreegraphLayout.createDummyNode(node, child, gapSize);
|
|
gapSize--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Reset the calculated values from the previous run.
|
|
* @param {TreegraphNode[]} nodes all of the nodes.
|
|
*/
|
|
resetValues(nodes) {
|
|
for (const node of nodes) {
|
|
node.mod = 0;
|
|
node.ancestor = node;
|
|
node.shift = 0;
|
|
node.thread = void 0;
|
|
node.change = 0;
|
|
node.preX = 0;
|
|
}
|
|
}
|
|
/**
|
|
* Assigns the value to each node, which indicates, what is his sibling
|
|
* number.
|
|
*
|
|
* @param {TreegraphNode} node
|
|
* Root node
|
|
* @param {number} index
|
|
* Index to which the nodes position should be set
|
|
*/
|
|
calculateRelativeX(node, index) {
|
|
const treeLayout = this, children = node.children;
|
|
for (let i = 0, iEnd = children.length; i < iEnd; ++i) {
|
|
treeLayout.calculateRelativeX(children[i], i);
|
|
}
|
|
node.relativeXPosition = index;
|
|
}
|
|
/**
|
|
* Recursive post order traversal of the tree, where the initial position
|
|
* of the nodes is calculated.
|
|
*
|
|
* @param {TreegraphNode} node
|
|
* The node for which the position should be calculated.
|
|
*/
|
|
firstWalk(node) {
|
|
const treeLayout = this,
|
|
// Arbitrary value used to position nodes in respect to each other.
|
|
siblingDistance = 1;
|
|
let leftSibling;
|
|
// If the node is a leaf, set it's position based on the left siblings.
|
|
if (!node.hasChildren()) {
|
|
leftSibling = node.getLeftSibling();
|
|
if (leftSibling) {
|
|
node.preX = leftSibling.preX + siblingDistance;
|
|
node.mod = node.preX;
|
|
}
|
|
else {
|
|
node.preX = 0;
|
|
}
|
|
}
|
|
else {
|
|
// If the node has children, perform the recursive first walk for
|
|
// its children, and then calculate its shift in the apportion
|
|
// function (most crucial part of the algorithm).
|
|
let defaultAncestor = node.getLeftMostChild();
|
|
for (const child of node.children) {
|
|
treeLayout.firstWalk(child);
|
|
defaultAncestor = treeLayout.apportion(child, defaultAncestor);
|
|
}
|
|
treeLayout.executeShifts(node);
|
|
const leftChild = node.getLeftMostChild(), rightChild = node.getRightMostChild(),
|
|
// Set the position of the parent as a middle point of its
|
|
// children and move it by the value of the leftSibling (if it
|
|
// exists).
|
|
midPoint = (leftChild.preX + rightChild.preX) / 2;
|
|
leftSibling = node.getLeftSibling();
|
|
if (leftSibling) {
|
|
node.preX = leftSibling.preX + siblingDistance;
|
|
node.mod = node.preX - midPoint;
|
|
}
|
|
else {
|
|
node.preX = midPoint;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Pre order traversal of the tree, which sets the final xPosition of the
|
|
* node as its preX value and sum of all if it's parents' modifiers.
|
|
*
|
|
* @param {TreegraphNode} node
|
|
* The node, for which the final position should be calculated.
|
|
* @param {number} modSum
|
|
* The sum of modifiers of all of the parents.
|
|
*/
|
|
secondWalk(node, modSum) {
|
|
const treeLayout = this;
|
|
// When the chart is not inverted we want the tree to be positioned from
|
|
// left to right with root node close to the chart border, this is why
|
|
// x and y positions are switched.
|
|
node.yPosition = node.preX + modSum;
|
|
node.xPosition = node.level;
|
|
for (const child of node.children) {
|
|
treeLayout.secondWalk(child, modSum + node.mod);
|
|
}
|
|
}
|
|
/**
|
|
* Shift all children of the current node from right to left.
|
|
*
|
|
* @param {TreegraphNode} node
|
|
* The parent node.
|
|
*/
|
|
executeShifts(node) {
|
|
let shift = 0, change = 0;
|
|
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
const childNode = node.children[i];
|
|
childNode.preX += shift;
|
|
childNode.mod += shift;
|
|
change += childNode.change;
|
|
shift += childNode.shift + change;
|
|
}
|
|
}
|
|
/**
|
|
* The core of the algorithm. The new subtree is combined with the previous
|
|
* subtrees. Threads are used to traverse the inside and outside contours of
|
|
* the left and right subtree up to the highest common level. The vertecies
|
|
* are left(right)Int(Out)node where Int means internal and Out means
|
|
* outernal. For summing up the modifiers along the contour we use the
|
|
* `left(right)Int(Out)mod` variable. Whenever two nodes of the inside
|
|
* contours are in conflict we commute the left one of the greatest uncommon
|
|
* ancestors using the getAncestor function and we call the moveSubtree
|
|
* method to shift the subtree and prepare the shifts of smaller subtrees.
|
|
* Finally we add a new thread (if necessary) and we adjust ancestor of
|
|
* right outernal node or defaultAncestor.
|
|
*
|
|
* @param {TreegraphNode} node
|
|
* @param {TreegraphNode} defaultAncestor
|
|
* The default ancestor of the passed node.
|
|
*/
|
|
apportion(node, defaultAncestor) {
|
|
const treeLayout = this, leftSibling = node.getLeftSibling();
|
|
if (leftSibling) {
|
|
let rightIntNode = node, rightOutNode = node, leftIntNode = leftSibling, leftOutNode = rightIntNode.getLeftMostSibling(), rightIntMod = rightIntNode.mod, rightOutMod = rightOutNode.mod, leftIntMod = leftIntNode.mod, leftOutMod = leftOutNode.mod;
|
|
while (leftIntNode &&
|
|
leftIntNode.nextRight() &&
|
|
rightIntNode &&
|
|
rightIntNode.nextLeft()) {
|
|
leftIntNode = leftIntNode.nextRight();
|
|
leftOutNode = leftOutNode.nextLeft();
|
|
rightIntNode = rightIntNode.nextLeft();
|
|
rightOutNode = rightOutNode.nextRight();
|
|
rightOutNode.ancestor = node;
|
|
const siblingDistance = 1, shift = leftIntNode.preX +
|
|
leftIntMod -
|
|
(rightIntNode.preX + rightIntMod) +
|
|
siblingDistance;
|
|
if (shift > 0) {
|
|
treeLayout.moveSubtree(node.getAncestor(leftIntNode, defaultAncestor), node, shift);
|
|
rightIntMod += shift;
|
|
rightOutMod += shift;
|
|
}
|
|
leftIntMod += leftIntNode.mod;
|
|
rightIntMod += rightIntNode.mod;
|
|
leftOutMod += leftOutNode.mod;
|
|
rightOutMod += rightOutNode.mod;
|
|
}
|
|
if (leftIntNode &&
|
|
leftIntNode.nextRight() &&
|
|
!rightOutNode.nextRight()) {
|
|
rightOutNode.thread = leftIntNode.nextRight();
|
|
rightOutNode.mod += leftIntMod - rightOutMod;
|
|
}
|
|
if (rightIntNode &&
|
|
rightIntNode.nextLeft() &&
|
|
!leftOutNode.nextLeft()) {
|
|
leftOutNode.thread = rightIntNode.nextLeft();
|
|
leftOutNode.mod += rightIntMod - leftOutMod;
|
|
}
|
|
defaultAncestor = node;
|
|
}
|
|
return defaultAncestor;
|
|
}
|
|
/**
|
|
* Shifts the subtree from leftNode to rightNode.
|
|
*
|
|
* @param {TreegraphNode} leftNode
|
|
* @param {TreegraphNode} rightNode
|
|
* @param {number} shift
|
|
* The value, by which the subtree should be moved.
|
|
*/
|
|
moveSubtree(leftNode, rightNode, shift) {
|
|
const subtrees = rightNode.relativeXPosition - leftNode.relativeXPosition;
|
|
rightNode.change -= shift / subtrees;
|
|
rightNode.shift += shift;
|
|
rightNode.preX += shift;
|
|
rightNode.mod += shift;
|
|
leftNode.change += shift / subtrees;
|
|
}
|
|
/**
|
|
* Clear values created in a beforeLayout.
|
|
*
|
|
* @param {TreegraphNode[]} nodes
|
|
* All of the nodes of the Treegraph Series.
|
|
*/
|
|
afterLayout(nodes) {
|
|
for (const node of nodes) {
|
|
if (node.oldParentNode) {
|
|
// Restore default connections
|
|
node.relativeXPosition = node.parentNode.relativeXPosition;
|
|
node.parent = node.oldParentNode.parent;
|
|
node.parentNode = node.oldParentNode;
|
|
// Delete dummyNode
|
|
delete node.oldParentNode.children[node.relativeXPosition];
|
|
node.oldParentNode.children[node.relativeXPosition] = node;
|
|
node.oldParentNode = void 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return TreegraphLayout;
|
|
});
|
|
_registerModule(_modules, 'Series/Treegraph/TreegraphSeriesDefaults.js', [], function () {
|
|
/* *
|
|
*
|
|
* (c) 2010-2024 Pawel Lysy Grzegorz Blachlinski
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
/* *
|
|
*
|
|
* Constants
|
|
*
|
|
* */
|
|
/**
|
|
* A treegraph series is a diagram, which shows a relation between ancestors
|
|
* and descendants with a clear parent - child relation.
|
|
* The best examples of the dataStructures, which best reflect this chart
|
|
* are e.g. genealogy tree or directory structure.
|
|
*
|
|
* TODO change back the demo path
|
|
* @sample highcharts/demo/treegraph-chart
|
|
* Treegraph Chart
|
|
*
|
|
* @extends plotOptions.treemap
|
|
* @excluding layoutAlgorithm, dashStyle, linecap, lineWidth,
|
|
* negativeColor, threshold, zones, zoneAxis, colorAxis,
|
|
* colorKey, compare, dataGrouping, endAngle, gapSize, gapUnit,
|
|
* ignoreHiddenPoint, innerSize, joinBy, legendType, linecap,
|
|
* minSize, navigatorOptions, pointRange, allowTraversingTree,
|
|
* alternateStartingDirection, borderRadius, breadcrumbs,
|
|
* interactByLeaf, layoutStartingDirection, levelIsConstant,
|
|
* lineWidth, negativeColor, nodes, sortIndex, zoneAxis,
|
|
* zones
|
|
*
|
|
* @product highcharts
|
|
* @since 10.3.0
|
|
* @requires modules/treemap.js
|
|
* @requires modules/treegraph.js
|
|
* @optionparent plotOptions.treegraph
|
|
*/
|
|
const TreegraphSeriesDefaults = {
|
|
/**
|
|
* Flips the positions of the nodes of a treegraph along the
|
|
* horizontal axis (vertical if chart is inverted).
|
|
*
|
|
* @sample highcharts/series-treegraph/reversed-nodes
|
|
* Treegraph series with reversed nodes.
|
|
*
|
|
* @type {boolean}
|
|
* @default false
|
|
* @product highcharts
|
|
* @since 10.3.0
|
|
*/
|
|
reversed: false,
|
|
/**
|
|
* @extends plotOptions.series.marker
|
|
* @excluding enabled, enabledThreshold
|
|
*/
|
|
marker: {
|
|
radius: 10,
|
|
lineWidth: 0,
|
|
symbol: 'circle',
|
|
fillOpacity: 1,
|
|
states: {}
|
|
},
|
|
link: {
|
|
/**
|
|
* Modifier of the shape of the curved link. Works best for
|
|
* values between 0 and 1, where 0 is a straight line, and 1 is
|
|
* a shape close to the default one.
|
|
*
|
|
* @type {number}
|
|
* @default 0.5
|
|
* @product highcharts
|
|
* @since 10.3.0
|
|
* @apioption series.treegraph.link.curveFactor
|
|
*/
|
|
/**
|
|
* The color of the links between nodes.
|
|
*
|
|
* @type {Highcharts.ColorString}
|
|
* @private
|
|
*/
|
|
color: "#666666" /* Palette.neutralColor60 */,
|
|
/**
|
|
* The line width of the links connecting nodes, in pixels.
|
|
* @type {number}
|
|
*
|
|
* @private
|
|
*/
|
|
lineWidth: 1,
|
|
/**
|
|
* Radius for the rounded corners of the links between nodes.
|
|
* Works for `default` link type.
|
|
*
|
|
* @private
|
|
*/
|
|
radius: 10,
|
|
cursor: 'default',
|
|
/**
|
|
* Type of the link shape.
|
|
*
|
|
* @sample highcharts/series-treegraph/link-types
|
|
* Different link types
|
|
*
|
|
* @type {'default' | 'curved' | 'straight'}
|
|
* @product highcharts
|
|
*
|
|
*/
|
|
type: 'curved'
|
|
},
|
|
/**
|
|
* Options applied to collapse Button. The collape button is the
|
|
* small button which indicates, that the node is collapsable.
|
|
*/
|
|
collapseButton: {
|
|
/**
|
|
* Whether the button should be visible only when the node is
|
|
* hovered. When set to true, the button is hidden for nodes,
|
|
* which are not collapsed, and shown for the collapsed ones.
|
|
*/
|
|
onlyOnHover: true,
|
|
/**
|
|
* Whether the button should be visible.
|
|
*/
|
|
enabled: true,
|
|
/**
|
|
* The line width of the button in pixels
|
|
*/
|
|
lineWidth: 1,
|
|
/**
|
|
* Offset of the button in the x direction.
|
|
*/
|
|
x: 0,
|
|
/**
|
|
* Offset of the button in the y direction.
|
|
*/
|
|
y: 0,
|
|
/**
|
|
* Height of the button.
|
|
*/
|
|
height: 18,
|
|
/**
|
|
* Width of the button.
|
|
*/
|
|
width: 18,
|
|
/**
|
|
* The symbol of the collapse button.
|
|
*/
|
|
shape: 'circle',
|
|
/**
|
|
* CSS styles for the collapse button.
|
|
*
|
|
* In styled mode, the collapse button style is given in the
|
|
* `.highcharts-collapse-button` class.
|
|
*/
|
|
style: {
|
|
cursor: 'pointer',
|
|
fontWeight: 'bold',
|
|
fontSize: '1em'
|
|
}
|
|
},
|
|
/**
|
|
* Whether the treegraph series should fill the entire plot area in the X
|
|
* axis direction, even when there are collapsed points.
|
|
*
|
|
* @sample highcharts/series-treegraph/fillspace
|
|
* Fill space demonstrated
|
|
*
|
|
* @product highcharts
|
|
*/
|
|
fillSpace: false,
|
|
/**
|
|
* @extends plotOptions.series.tooltip
|
|
*/
|
|
tooltip: {
|
|
/**
|
|
* The HTML of the point's line in the tooltip. Variables are
|
|
* enclosed by curly brackets. Available variables are
|
|
* `point.id`, `point.fromNode.id`, `point.toNode.id`,
|
|
* `series.name`, `series.color` and other properties on the
|
|
* same form. Furthermore, This can also be overridden for each
|
|
* series, which makes it a good hook for displaying units. In
|
|
* styled mode, the dot is colored by a class name rather than
|
|
* the point color.
|
|
*
|
|
* @type {string}
|
|
* @since 10.3.0
|
|
* @product highcharts
|
|
*/
|
|
linkFormat: '{point.fromNode.id} \u2192 {point.toNode.id}',
|
|
pointFormat: '{point.id}'
|
|
/**
|
|
* A callback function for formatting the HTML output for a
|
|
* single link in the tooltip. Like the `linkFormat` string,
|
|
* but with more flexibility.
|
|
*
|
|
* @type {Highcharts.FormatterCallbackFunction.<Highcharts.Point>}
|
|
* @apioption series.treegraph.tooltip.linkFormatter
|
|
*
|
|
*/
|
|
},
|
|
/**
|
|
* Options for the data labels appearing on top of the nodes and
|
|
* links. For treegraph charts, data labels are visible for the
|
|
* nodes by default, but hidden for links. This is controlled by
|
|
* modifying the `nodeFormat`, and the `format` that applies to
|
|
* links and is an empty string by default.
|
|
*
|
|
* @declare Highcharts.SeriesTreegraphDataLabelsOptionsObject
|
|
*/
|
|
dataLabels: {
|
|
defer: true,
|
|
/**
|
|
* Options for a _link_ label text which should follow link
|
|
* connection. Border and background are disabled for a label
|
|
* that follows a path.
|
|
*
|
|
* **Note:** Only SVG-based renderer supports this option.
|
|
* Setting `useHTML` to true will disable this option.
|
|
*
|
|
* @sample highcharts/series-treegraph/link-text-path
|
|
* Treegraph series with link text path dataLabels.
|
|
*
|
|
* @extends plotOptions.treegraph.dataLabels.textPath
|
|
* @since 10.3.0
|
|
*/
|
|
linkTextPath: {
|
|
attributes: {
|
|
startOffset: '50%'
|
|
}
|
|
},
|
|
enabled: true,
|
|
linkFormatter: () => '',
|
|
style: {
|
|
textOverflow: 'none'
|
|
}
|
|
},
|
|
/**
|
|
* The distance between nodes in a tree graph in the longitudinal direction.
|
|
* The longitudinal direction means the direction that the chart flows - in
|
|
* a horizontal chart the distance is horizontal, in an inverted chart
|
|
* (vertical), the distance is vertical.
|
|
*
|
|
* If a number is given, it denotes pixels. If a percentage string is given,
|
|
* the distance is a percentage of the rendered node width. A `nodeDistance`
|
|
* of `100%` will render equal widths for the nodes and the gaps between
|
|
* them.
|
|
*
|
|
* This option applies only when the `nodeWidth` option is `auto`, making
|
|
* the node width respond to the number of columns.
|
|
*
|
|
* @since 11.4.0
|
|
* @sample highcharts/series-treegraph/node-distance
|
|
* Node distance of 100% means equal to node width
|
|
* @type {number|string}
|
|
*/
|
|
nodeDistance: 30,
|
|
/**
|
|
* The pixel width of each node in a, or the height in case the chart is
|
|
* inverted. For tree graphs, the node width is only applied if the marker
|
|
* symbol is `rect`, otherwise the `marker` sizing options apply.
|
|
*
|
|
* Can be a number or a percentage string, or `auto`. If `auto`, the nodes
|
|
* are sized to fill up the plot area in the longitudinal direction,
|
|
* regardless of the number of levels.
|
|
*
|
|
* @since 11.4.0
|
|
* @see [treegraph.nodeDistance](#nodeDistance)
|
|
* @sample highcharts/series-treegraph/node-distance
|
|
* Node width is auto and combined with node distance
|
|
*
|
|
* @type {number|string}
|
|
*/
|
|
nodeWidth: void 0
|
|
};
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return TreegraphSeriesDefaults;
|
|
});
|
|
_registerModule(_modules, 'Series/Treegraph/TreegraphSeries.js', [_modules['Series/PathUtilities.js'], _modules['Core/Series/SeriesRegistry.js'], _modules['Core/Renderer/SVG/SVGRenderer.js'], _modules['Series/Treegraph/TreegraphNode.js'], _modules['Series/Treegraph/TreegraphPoint.js'], _modules['Series/TreeUtilities.js'], _modules['Core/Utilities.js'], _modules['Series/Treegraph/TreegraphLink.js'], _modules['Series/Treegraph/TreegraphLayout.js'], _modules['Series/Treegraph/TreegraphSeriesDefaults.js']], function (PU, SeriesRegistry, SVGRenderer, TreegraphNode, TreegraphPoint, TU, U, TreegraphLink, TreegraphLayout, TreegraphSeriesDefaults) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2024 Pawel Lysy Grzegorz Blachlinski
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { getLinkPath } = PU;
|
|
const { series: { prototype: seriesProto }, seriesTypes: { treemap: TreemapSeries, column: ColumnSeries } } = SeriesRegistry;
|
|
const { prototype: { symbols } } = SVGRenderer;
|
|
const { getLevelOptions, getNodeWidth } = TU;
|
|
const { arrayMax, extend, merge, pick, relativeLength, splat } = U;
|
|
/* *
|
|
*
|
|
* Class
|
|
*
|
|
* */
|
|
/**
|
|
* The Treegraph series type.
|
|
*
|
|
* @private
|
|
* @class
|
|
* @name Highcharts.seriesTypes.treegraph
|
|
*
|
|
* @augments Highcharts.Series
|
|
*/
|
|
class TreegraphSeries extends TreemapSeries {
|
|
constructor() {
|
|
/* *
|
|
*
|
|
* Static Properties
|
|
*
|
|
* */
|
|
super(...arguments);
|
|
this.nodeList = [];
|
|
this.links = [];
|
|
}
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
init() {
|
|
super.init.apply(this, arguments);
|
|
this.layoutAlgorythm = new TreegraphLayout();
|
|
}
|
|
/**
|
|
* Calculate `a` and `b` parameters of linear transformation, where
|
|
* `finalPosition = a * calculatedPosition + b`.
|
|
*
|
|
* @return {LayoutModifiers} `a` and `b` parameter for x and y direction.
|
|
*/
|
|
getLayoutModifiers() {
|
|
const chart = this.chart, series = this, plotSizeX = chart.plotSizeX, plotSizeY = chart.plotSizeY, columnCount = arrayMax(this.points.map((p) => p.node.xPosition));
|
|
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, maxXSize = 0, minXSize = 0, maxYSize = 0, minYSize = 0;
|
|
this.points.forEach((point) => {
|
|
// When fillSpace is on, stop the layout calculation when the hidden
|
|
// points are reached. (#19038)
|
|
if (this.options.fillSpace && !point.visible) {
|
|
return;
|
|
}
|
|
const node = point.node, level = series.mapOptionsToLevel[point.node.level] || {}, markerOptions = merge(this.options.marker, level.marker, point.options.marker), nodeWidth = markerOptions.width ?? getNodeWidth(this, columnCount), radius = relativeLength(markerOptions.radius || 0, Math.min(plotSizeX, plotSizeY)), symbol = markerOptions.symbol, nodeSizeY = (symbol === 'circle' || !markerOptions.height) ?
|
|
radius * 2 :
|
|
relativeLength(markerOptions.height, plotSizeY), nodeSizeX = symbol === 'circle' || !nodeWidth ?
|
|
radius * 2 :
|
|
relativeLength(nodeWidth, plotSizeX);
|
|
node.nodeSizeX = nodeSizeX;
|
|
node.nodeSizeY = nodeSizeY;
|
|
let lineWidth;
|
|
if (node.xPosition <= minX) {
|
|
minX = node.xPosition;
|
|
lineWidth = markerOptions.lineWidth || 0;
|
|
minXSize = Math.max(nodeSizeX + lineWidth, minXSize);
|
|
}
|
|
if (node.xPosition >= maxX) {
|
|
maxX = node.xPosition;
|
|
lineWidth = markerOptions.lineWidth || 0;
|
|
maxXSize = Math.max(nodeSizeX + lineWidth, maxXSize);
|
|
}
|
|
if (node.yPosition <= minY) {
|
|
minY = node.yPosition;
|
|
lineWidth = markerOptions.lineWidth || 0;
|
|
minYSize = Math.max(nodeSizeY + lineWidth, minYSize);
|
|
}
|
|
if (node.yPosition >= maxY) {
|
|
maxY = node.yPosition;
|
|
lineWidth = markerOptions.lineWidth || 0;
|
|
maxYSize = Math.max(nodeSizeY + lineWidth, maxYSize);
|
|
}
|
|
});
|
|
// Calculate the values of linear transformation, which will later be
|
|
// applied as `nodePosition = a * x + b` for each direction.
|
|
const ay = maxY === minY ?
|
|
1 :
|
|
(plotSizeY - (minYSize + maxYSize) / 2) / (maxY - minY), by = maxY === minY ? plotSizeY / 2 : -ay * minY + minYSize / 2, ax = maxX === minX ?
|
|
1 :
|
|
(plotSizeX - (maxXSize + maxXSize) / 2) / (maxX - minX), bx = maxX === minX ? plotSizeX / 2 : -ax * minX + minXSize / 2;
|
|
return { ax, bx, ay, by };
|
|
}
|
|
getLinks() {
|
|
const series = this;
|
|
const links = [];
|
|
this.data.forEach((point) => {
|
|
const levelOptions = series.mapOptionsToLevel[point.node.level || 0] || {};
|
|
if (point.node.parent) {
|
|
const pointOptions = merge(levelOptions, point.options);
|
|
if (!point.linkToParent || point.linkToParent.destroyed) {
|
|
const link = new series.LinkClass(series, pointOptions, void 0, point);
|
|
point.linkToParent = link;
|
|
}
|
|
else {
|
|
// #19552
|
|
point.collapsed = pick(point.collapsed, (this.mapOptionsToLevel[point.node.level] || {}).collapsed);
|
|
point.linkToParent.visible =
|
|
point.linkToParent.toNode.visible;
|
|
}
|
|
point.linkToParent.index = links.push(point.linkToParent) - 1;
|
|
}
|
|
else {
|
|
if (point.linkToParent) {
|
|
series.links.splice(point.linkToParent.index);
|
|
point.linkToParent.destroy();
|
|
delete point.linkToParent;
|
|
}
|
|
}
|
|
});
|
|
return links;
|
|
}
|
|
buildTree(id, index, level, list, parent) {
|
|
const point = this.points[index];
|
|
level = (point && point.level) || level;
|
|
return super.buildTree.call(this, id, index, level, list, parent);
|
|
}
|
|
markerAttribs() {
|
|
// The super Series.markerAttribs returns { width: NaN, height: NaN },
|
|
// so just disable this for now.
|
|
return {};
|
|
}
|
|
setCollapsedStatus(node, visibility) {
|
|
const point = node.point;
|
|
if (point) {
|
|
// Take the level options into account.
|
|
point.collapsed = pick(point.collapsed, (this.mapOptionsToLevel[node.level] || {}).collapsed);
|
|
point.visible = visibility;
|
|
visibility = visibility === false ? false : !point.collapsed;
|
|
}
|
|
node.children.forEach((childNode) => {
|
|
this.setCollapsedStatus(childNode, visibility);
|
|
});
|
|
}
|
|
drawTracker() {
|
|
ColumnSeries.prototype.drawTracker.apply(this, arguments);
|
|
ColumnSeries.prototype.drawTracker.call(this, this.links);
|
|
}
|
|
/**
|
|
* Run pre-translation by generating the nodeColumns.
|
|
* @private
|
|
*/
|
|
translate() {
|
|
const series = this, options = series.options;
|
|
// NOTE: updateRootId modifies series.
|
|
let rootId = TU.updateRootId(series), rootNode;
|
|
// Call prototype function
|
|
seriesProto.translate.call(series);
|
|
const tree = series.tree = series.getTree();
|
|
rootNode = series.nodeMap[rootId];
|
|
if (rootId !== '' && (!rootNode || !rootNode.children.length)) {
|
|
series.setRootNode('', false);
|
|
rootId = series.rootNode;
|
|
rootNode = series.nodeMap[rootId];
|
|
}
|
|
series.mapOptionsToLevel = getLevelOptions({
|
|
from: rootNode.level + 1,
|
|
levels: options.levels,
|
|
to: tree.height,
|
|
defaults: {
|
|
levelIsConstant: series.options.levelIsConstant,
|
|
colorByPoint: options.colorByPoint
|
|
}
|
|
});
|
|
this.setCollapsedStatus(tree, true);
|
|
series.links = series.getLinks();
|
|
series.setTreeValues(tree);
|
|
this.layoutAlgorythm.calculatePositions(series);
|
|
series.layoutModifier = this.getLayoutModifiers();
|
|
this.points.forEach((point) => {
|
|
this.translateNode(point);
|
|
});
|
|
this.points.forEach((point) => {
|
|
if (point.linkToParent) {
|
|
this.translateLink(point.linkToParent);
|
|
}
|
|
});
|
|
if (!options.colorByPoint) {
|
|
series.setColorRecursive(series.tree);
|
|
}
|
|
}
|
|
translateLink(link) {
|
|
const fromNode = link.fromNode, toNode = link.toNode, linkWidth = this.options.link.lineWidth, crisp = (Math.round(linkWidth) % 2) / 2, factor = pick(this.options.link.curveFactor, 0.5), type = pick(link.options.link && link.options.link.type, this.options.link.type);
|
|
if (fromNode.shapeArgs && toNode.shapeArgs) {
|
|
const fromNodeWidth = (fromNode.shapeArgs.width || 0), inverted = this.chart.inverted, y1 = Math.floor((fromNode.shapeArgs.y || 0) +
|
|
(fromNode.shapeArgs.height || 0) / 2) + crisp, y2 = Math.floor((toNode.shapeArgs.y || 0) +
|
|
(toNode.shapeArgs.height || 0) / 2) + crisp;
|
|
let x1 = Math.floor((fromNode.shapeArgs.x || 0) + fromNodeWidth) +
|
|
crisp, x2 = Math.floor(toNode.shapeArgs.x || 0) + crisp;
|
|
if (inverted) {
|
|
x1 -= fromNodeWidth;
|
|
x2 += (toNode.shapeArgs.width || 0);
|
|
}
|
|
const diff = toNode.node.xPosition - fromNode.node.xPosition;
|
|
link.shapeType = 'path';
|
|
const fullWidth = Math.abs(x2 - x1) + fromNodeWidth, width = (fullWidth / diff) - fromNodeWidth, offset = width * factor * (inverted ? -1 : 1);
|
|
const xMiddle = Math.floor((x2 + x1) / 2) + crisp;
|
|
link.plotX = xMiddle;
|
|
link.plotY = y2;
|
|
link.shapeArgs = {
|
|
d: getLinkPath[type]({
|
|
x1,
|
|
y1,
|
|
x2,
|
|
y2,
|
|
width,
|
|
offset,
|
|
inverted,
|
|
parentVisible: toNode.visible,
|
|
radius: this.options.link.radius
|
|
})
|
|
};
|
|
link.dlBox = {
|
|
x: (x1 + x2) / 2,
|
|
y: (y1 + y2) / 2,
|
|
height: linkWidth,
|
|
width: 0
|
|
};
|
|
link.tooltipPos = inverted ? [
|
|
(this.chart.plotSizeY || 0) - link.dlBox.y,
|
|
(this.chart.plotSizeX || 0) - link.dlBox.x
|
|
] : [
|
|
link.dlBox.x,
|
|
link.dlBox.y
|
|
];
|
|
}
|
|
}
|
|
/**
|
|
* Private method responsible for adjusting the dataLabel options for each
|
|
* node-point individually.
|
|
*/
|
|
drawNodeLabels(points) {
|
|
const series = this, mapOptionsToLevel = series.mapOptionsToLevel;
|
|
let options, level;
|
|
for (const point of points) {
|
|
level = mapOptionsToLevel[point.node.level];
|
|
// Set options to new object to avoid problems with scope
|
|
options = { style: {} };
|
|
// If options for level exists, include them as well
|
|
if (level && level.dataLabels) {
|
|
options = merge(options, level.dataLabels);
|
|
series.hasDataLabels = () => true;
|
|
}
|
|
// Set dataLabel width to the width of the point shape.
|
|
if (point.shapeArgs &&
|
|
!splat(series.options.dataLabels)[0].style.width) {
|
|
options.style.width = point.shapeArgs.width;
|
|
if (point.dataLabel) {
|
|
point.dataLabel.css({
|
|
width: point.shapeArgs.width + 'px'
|
|
});
|
|
}
|
|
}
|
|
// Merge custom options with point options
|
|
point.dlOptions = merge(options, point.options.dataLabels);
|
|
}
|
|
seriesProto.drawDataLabels.call(this, points);
|
|
}
|
|
/**
|
|
* Override alignDataLabel so that position is always calculated and the
|
|
* label is faded in and out instead of hidden/shown when collapsing and
|
|
* expanding nodes.
|
|
*/
|
|
alignDataLabel(point, dataLabel) {
|
|
const visible = point.visible;
|
|
// Force position calculation and visibility
|
|
point.visible = true;
|
|
super.alignDataLabel.apply(this, arguments);
|
|
// Fade in or out
|
|
dataLabel.animate({
|
|
opacity: visible === false ? 0 : 1
|
|
}, void 0, function () {
|
|
// Hide data labels that belong to hidden points (#18891)
|
|
visible || dataLabel.hide();
|
|
});
|
|
// Reset
|
|
point.visible = visible;
|
|
}
|
|
/**
|
|
* Treegraph has two separate collecions of nodes and lines,
|
|
* render dataLabels for both sets.
|
|
*/
|
|
drawDataLabels() {
|
|
if (this.options.dataLabels) {
|
|
this.options.dataLabels = splat(this.options.dataLabels);
|
|
// Render node labels.
|
|
this.drawNodeLabels(this.points);
|
|
// Render link labels.
|
|
seriesProto.drawDataLabels.call(this, this.links);
|
|
}
|
|
}
|
|
destroy() {
|
|
// Links must also be destroyed.
|
|
if (this.links) {
|
|
for (const link of this.links) {
|
|
link.destroy();
|
|
}
|
|
this.links.length = 0;
|
|
}
|
|
return seriesProto.destroy.apply(this, arguments);
|
|
}
|
|
/**
|
|
* Return the presentational attributes.
|
|
* @private
|
|
*/
|
|
pointAttribs(point, state) {
|
|
const series = this, levelOptions = point &&
|
|
series.mapOptionsToLevel[point.node.level || 0] || {}, options = point && point.options, stateOptions = (levelOptions.states &&
|
|
levelOptions.states[state]) ||
|
|
{};
|
|
if (point) {
|
|
point.options.marker = merge(series.options.marker, levelOptions.marker, point.options.marker);
|
|
}
|
|
const linkColor = pick(stateOptions && stateOptions.link && stateOptions.link.color, options && options.link && options.link.color, levelOptions && levelOptions.link && levelOptions.link.color, series.options.link && series.options.link.color), linkLineWidth = pick(stateOptions && stateOptions.link &&
|
|
stateOptions.link.lineWidth, options && options.link && options.link.lineWidth, levelOptions && levelOptions.link &&
|
|
levelOptions.link.lineWidth, series.options.link && series.options.link.lineWidth), attribs = seriesProto.pointAttribs.call(series, point, state);
|
|
if (point) {
|
|
if (point.isLink) {
|
|
attribs.stroke = linkColor;
|
|
attribs['stroke-width'] = linkLineWidth;
|
|
delete attribs.fill;
|
|
}
|
|
if (!point.visible) {
|
|
attribs.opacity = 0;
|
|
}
|
|
}
|
|
return attribs;
|
|
}
|
|
drawPoints() {
|
|
TreemapSeries.prototype.drawPoints.apply(this, arguments);
|
|
ColumnSeries.prototype.drawPoints.call(this, this.links);
|
|
}
|
|
/**
|
|
* Run translation operations for one node.
|
|
* @private
|
|
*/
|
|
translateNode(point) {
|
|
const chart = this.chart, node = point.node, plotSizeY = chart.plotSizeY, plotSizeX = chart.plotSizeX,
|
|
// Get the layout modifiers which are common for all nodes.
|
|
{ ax, bx, ay, by } = this.layoutModifier, x = ax * node.xPosition + bx, y = ay * node.yPosition + by, level = this.mapOptionsToLevel[node.level] || {}, markerOptions = merge(this.options.marker, level.marker, point.options.marker), symbol = markerOptions.symbol, height = node.nodeSizeY, width = node.nodeSizeX, reversed = this.options.reversed, nodeX = node.x = (chart.inverted ?
|
|
plotSizeX - width / 2 - x :
|
|
x - width / 2), nodeY = node.y = (!reversed ?
|
|
plotSizeY - y - height / 2 :
|
|
y - height / 2), borderRadius = pick(point.options.borderRadius, level.borderRadius, this.options.borderRadius), symbolFn = symbols[symbol || 'circle'];
|
|
if (symbolFn === void 0) {
|
|
point.hasImage = true;
|
|
point.shapeType = 'image';
|
|
point.imageUrl = symbol.match(/^url\((.*?)\)$/)[1];
|
|
}
|
|
else {
|
|
point.shapeType = 'path';
|
|
}
|
|
if (!point.visible && point.linkToParent) {
|
|
const parentNode = point.linkToParent.fromNode;
|
|
if (parentNode) {
|
|
const parentShapeArgs = parentNode.shapeArgs || {}, { x = 0, y = 0, width = 0, height = 0 } = parentShapeArgs;
|
|
if (!point.shapeArgs) {
|
|
point.shapeArgs = {};
|
|
}
|
|
if (!point.hasImage) {
|
|
extend(point.shapeArgs, {
|
|
d: symbolFn(x, y, width, height, borderRadius ? { r: borderRadius } : void 0)
|
|
});
|
|
}
|
|
extend(point.shapeArgs, { x, y });
|
|
point.plotX = parentNode.plotX;
|
|
point.plotY = parentNode.plotY;
|
|
}
|
|
}
|
|
else {
|
|
point.plotX = nodeX;
|
|
point.plotY = nodeY;
|
|
point.shapeArgs = {
|
|
x: nodeX,
|
|
y: nodeY,
|
|
width,
|
|
height,
|
|
cursor: !point.node.isLeaf ? 'pointer' : 'default'
|
|
};
|
|
if (!point.hasImage) {
|
|
point.shapeArgs.d = symbolFn(nodeX, nodeY, width, height, borderRadius ? { r: borderRadius } : void 0);
|
|
}
|
|
}
|
|
// Set the anchor position for tooltip.
|
|
point.tooltipPos = chart.inverted ?
|
|
[plotSizeY - nodeY - height / 2, plotSizeX - nodeX - width / 2] :
|
|
[nodeX + width / 2, nodeY];
|
|
}
|
|
}
|
|
TreegraphSeries.defaultOptions = merge(TreemapSeries.defaultOptions, TreegraphSeriesDefaults);
|
|
extend(TreegraphSeries.prototype, {
|
|
pointClass: TreegraphPoint,
|
|
NodeClass: TreegraphNode,
|
|
LinkClass: TreegraphLink
|
|
});
|
|
SeriesRegistry.registerSeriesType('treegraph', TreegraphSeries);
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
/* *
|
|
*
|
|
* API Options
|
|
*
|
|
* */
|
|
/**
|
|
* A `treegraph` series. If the [type](#series.treegraph.type)
|
|
* option is not specified, it is inherited from [chart.type](#chart.type).
|
|
*
|
|
* @extends series,plotOptions.treegraph
|
|
* @exclude allowDrillToNode, boostBlending, boostThreshold, curveFactor,
|
|
* centerInCategory, connectEnds, connectNulls, colorAxis, colorKey,
|
|
* dataSorting, dragDrop, findNearestPointBy, getExtremesFromAll, layout,
|
|
* nodePadding, pointInterval, pointIntervalUnit, pointPlacement, pointStart,
|
|
* relativeXValue, softThreshold, stack, stacking, step,
|
|
* traverseUpButton, xAxis, yAxis, zoneAxis, zones
|
|
* @product highcharts
|
|
* @requires modules/treemap.js
|
|
* @requires modules/treegraph.js
|
|
* @apioption series.treegraph
|
|
*/
|
|
/**
|
|
* @extends plotOptions.series.marker
|
|
* @excluding enabled, enabledThreshold
|
|
* @apioption series.treegraph.marker
|
|
*/
|
|
/**
|
|
* @type {Highcharts.SeriesTreegraphDataLabelsOptionsObject|Array<Highcharts.SeriesTreegraphDataLabelsOptionsObject>}
|
|
* @product highcharts
|
|
* @apioption series.treegraph.data.dataLabels
|
|
*/
|
|
/**
|
|
* @sample highcharts/series-treegraph/level-options
|
|
* Treegraph chart with level options applied
|
|
*
|
|
* @type {Array<*>}
|
|
* @excluding layoutStartingDirection, layoutAlgorithm
|
|
* @apioption series.treegraph.levels
|
|
*/
|
|
/**
|
|
* Set collapsed status for nodes level-wise.
|
|
* @type {boolean}
|
|
* @apioption series.treegraph.levels.collapsed
|
|
*/
|
|
/**
|
|
* Set marker options for nodes at the level.
|
|
* @extends series.treegraph.marker
|
|
* @apioption series.treegraph.levels.marker
|
|
*/
|
|
/**
|
|
* An array of data points for the series. For the `treegraph` series type,
|
|
* points can be given in the following ways:
|
|
*
|
|
* 1. The array of arrays, with `keys` property, which defines how the fields in
|
|
* array should be interpreted
|
|
* ```js
|
|
* keys: ['id', 'parent'],
|
|
* data: [
|
|
* ['Category1'],
|
|
* ['Category1', 'Category2']
|
|
* ]
|
|
*
|
|
* 2. 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.
|
|
* The data of the treegraph series needs to be formatted in such a way, that
|
|
* there are no circular dependencies on the nodes
|
|
*
|
|
* ```js
|
|
* data: [{
|
|
* id: 'Category1'
|
|
* }, {
|
|
* id: 'Category1',
|
|
* parent: 'Category2',
|
|
* }]
|
|
* ```
|
|
*
|
|
* @type {Array<*>}
|
|
* @extends series.treemap.data
|
|
* @product highcharts
|
|
* @excluding outgoing, weight, value
|
|
* @apioption series.treegraph.data
|
|
*/
|
|
/**
|
|
* Options used for button, which toggles the collapse status of the node.
|
|
*
|
|
*
|
|
* @apioption series.treegraph.data.collapseButton
|
|
*/
|
|
/**
|
|
* If point's children should be initially hidden
|
|
*
|
|
* @sample highcharts/series-treegraph/level-options
|
|
* Treegraph chart with initially hidden children
|
|
*
|
|
* @type {boolean}
|
|
* @apioption series.treegraph.data.collapsed
|
|
*/
|
|
''; // Gets doclets above into transpiled version
|
|
|
|
return TreegraphSeries;
|
|
});
|
|
_registerModule(_modules, 'masters/modules/treegraph.src.js', [_modules['Core/Globals.js']], function (Highcharts) {
|
|
|
|
|
|
return Highcharts;
|
|
});
|
|
})); |