UNPKG

highcharts

Version:
1,202 lines (1,196 loc) 86.6 kB
/** * @license Highcharts JS v10.0.0 (2022-03-07) * * Sankey diagram module * * (c) 2010-2021 Torstein Honsi * * 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/sankey', ['highcharts'], 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/NodesComposition.js', [_modules['Core/Series/Point.js'], _modules['Core/Series/Series.js'], _modules['Core/Utilities.js']], function (Point, Series, U) { /* * * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var defined = U.defined, extend = U.extend, find = U.find, merge = U.merge, pick = U.pick; /* * * * Composition * * */ var NodesComposition; (function (NodesComposition) { /* * * * Declarations * * */ /* * * * Constants * * */ var composedClasses = []; /* * * * Functions * * */ /* eslint-disable valid-jsdoc */ /** * @private */ function compose(PointClass, SeriesClass) { if (composedClasses.indexOf(PointClass) === -1) { composedClasses.push(PointClass); var pointProto = PointClass.prototype; pointProto.setNodeState = setNodeState; pointProto.setState = setNodeState; pointProto.update = updateNode; } if (composedClasses.indexOf(SeriesClass) === -1) { composedClasses.push(SeriesClass); var seriesProto = SeriesClass.prototype; seriesProto.destroy = destroy; seriesProto.setData = setData; } return SeriesClass; } NodesComposition.compose = compose; /** * Create a single node that holds information on incoming and outgoing * links. * @private */ function createNode(id) { var PointClass = this.pointClass, findById = function (nodes, id) { return find(nodes, function (node) { return node.id === id; }); }; var node = findById(this.nodes, id), options; if (!node) { options = this.options.nodes && findById(this.options.nodes, id); node = (new PointClass()).init(this, extend({ className: 'highcharts-node', isNode: true, id: id, y: 1 // Pass isNull test }, options)); node.linksTo = []; node.linksFrom = []; /** * Return the largest sum of either the incoming or outgoing links. * @private */ node.getSum = function () { var sumTo = 0, sumFrom = 0; node.linksTo.forEach(function (link) { sumTo += link.weight; }); node.linksFrom.forEach(function (link) { sumFrom += link.weight; }); return Math.max(sumTo, sumFrom); }; /** * Get the offset in weight values of a point/link. * @private */ node.offset = function (point, coll) { var offset = 0; for (var i = 0; i < node[coll].length; i++) { if (node[coll][i] === point) { return offset; } offset += node[coll][i].weight; } }; // Return true if the node has a shape, otherwise all links are // outgoing. node.hasShape = function () { var outgoing = 0; node.linksTo.forEach(function (link) { if (link.outgoing) { outgoing++; } }); return (!node.linksTo.length || outgoing !== node.linksTo.length); }; node.index = this.nodes.push(node) - 1; } node.formatPrefix = 'node'; // for use in formats node.name = node.name || node.options.id || ''; // Mass is used in networkgraph: node.mass = pick( // Node: node.options.mass, node.options.marker && node.options.marker.radius, // Series: this.options.marker && this.options.marker.radius, // Default: 4); return node; } NodesComposition.createNode = createNode; /** * Destroy alll nodes and links. * @private */ function destroy() { // Nodes must also be destroyed (#8682, #9300) this.data = [] .concat(this.points || [], this.nodes); return Series.prototype.destroy.apply(this, arguments); } NodesComposition.destroy = destroy; /** * Extend generatePoints by adding the nodes, which are Point objects * but pushed to the this.nodes array. */ function generatePoints() { var chart = this.chart, nodeLookup = {}; Series.prototype.generatePoints.call(this); if (!this.nodes) { this.nodes = []; // List of Point-like node items } this.colorCounter = 0; // Reset links from previous run this.nodes.forEach(function (node) { node.linksFrom.length = 0; node.linksTo.length = 0; node.level = node.options.level; }); // Create the node list and set up links this.points.forEach(function (point) { if (defined(point.from)) { if (!nodeLookup[point.from]) { nodeLookup[point.from] = this.createNode(point.from); } nodeLookup[point.from].linksFrom.push(point); point.fromNode = nodeLookup[point.from]; // Point color defaults to the fromNode's color if (chart.styledMode) { point.colorIndex = pick(point.options.colorIndex, nodeLookup[point.from].colorIndex); } else { point.color = point.options.color || nodeLookup[point.from].color; } } if (defined(point.to)) { if (!nodeLookup[point.to]) { nodeLookup[point.to] = this.createNode(point.to); } nodeLookup[point.to].linksTo.push(point); point.toNode = nodeLookup[point.to]; } point.name = point.name || point.id; // for use in formats }, this); // Store lookup table for later use this.nodeLookup = nodeLookup; } NodesComposition.generatePoints = generatePoints; /** * Destroy all nodes on setting new data * @private */ function setData() { if (this.nodes) { this.nodes.forEach(function (node) { node.destroy(); }); this.nodes.length = 0; } Series.prototype.setData.apply(this, arguments); } /** * When hovering node, highlight all connected links. When hovering a link, * highlight all connected nodes. */ function setNodeState(state) { var args = arguments, others = this.isNode ? this.linksTo.concat(this.linksFrom) : [this.fromNode, this.toNode]; if (state !== 'select') { others.forEach(function (linkOrNode) { if (linkOrNode && linkOrNode.series) { Point.prototype.setState.apply(linkOrNode, args); if (!linkOrNode.isNode) { if (linkOrNode.fromNode.graphic) { Point.prototype.setState.apply(linkOrNode.fromNode, args); } if (linkOrNode.toNode && linkOrNode.toNode.graphic) { Point.prototype.setState.apply(linkOrNode.toNode, args); } } } }); } Point.prototype.setState.apply(this, args); } NodesComposition.setNodeState = setNodeState; /** * When updating a node, don't update `series.options.data`, but `series.options.nodes` */ function updateNode(options, redraw, animation, runEvent) { var _this = this; var nodes = this.series.options.nodes, data = this.series.options.data, dataLength = data && data.length || 0, linkConfig = data && data[this.index]; Point.prototype.update.call(this, options, this.isNode ? false : redraw, // Hold the redraw for nodes animation, runEvent); if (this.isNode) { // this.index refers to `series.nodes`, not `options.nodes` array var nodeIndex = (nodes || []) .reduce(// Array.findIndex needs a polyfill function (prevIndex, n, index) { return (_this.id === n.id ? index : prevIndex); }, -1), // Merge old config with new config. New config is stored in // options.data, because of default logic in point.update() nodeConfig = merge(nodes && nodes[nodeIndex] || {}, data && data[this.index] || {}); // Restore link config if (data) { if (linkConfig) { data[this.index] = linkConfig; } else { // Remove node from config if there's more nodes than links data.length = dataLength; } } // Set node config if (nodes) { if (nodeIndex >= 0) { nodes[nodeIndex] = nodeConfig; } else { nodes.push(nodeConfig); } } else { this.series.options.nodes = [nodeConfig]; } if (pick(redraw, true)) { this.series.chart.redraw(animation); } } } NodesComposition.updateNode = updateNode; })(NodesComposition || (NodesComposition = {})); /* * * * Default Export * * */ return NodesComposition; }); _registerModule(_modules, 'Series/Sankey/SankeyPoint.js', [_modules['Core/Series/Point.js'], _modules['Core/Series/SeriesRegistry.js'], _modules['Core/Utilities.js']], function (Point, SeriesRegistry, U) { /* * * * Sankey diagram module * * (c) 2010-2021 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var ColumnSeries = SeriesRegistry.seriesTypes.column; var defined = U.defined; /* * * * Class * * */ var SankeyPoint = /** @class */ (function (_super) { __extends(SankeyPoint, _super); function SankeyPoint() { /* * * * Properties * * */ var _this = _super !== null && _super.apply(this, arguments) || this; _this.className = void 0; _this.fromNode = void 0; _this.level = void 0; _this.linkBase = void 0; _this.linksFrom = void 0; _this.linksTo = void 0; _this.mass = void 0; _this.nodeX = void 0; _this.nodeY = void 0; _this.options = void 0; _this.series = void 0; _this.toNode = void 0; return _this; /* eslint-enable valid-jsdoc */ } /* * * * Functions * * */ /* eslint-disable valid-jsdoc */ /** * @private */ SankeyPoint.prototype.applyOptions = function (options, x) { Point.prototype.applyOptions.call(this, options, x); // Treat point.level as a synonym of point.column if (defined(this.options.level)) { this.options.column = this.column = this.options.level; } return this; }; /** * @private */ SankeyPoint.prototype.getClassName = function () { return (this.isNode ? 'highcharts-node ' : 'highcharts-link ') + Point.prototype.getClassName.call(this); }; /** * If there are incoming links, place it to the right of the * highest order column that links to this one. * * @private */ SankeyPoint.prototype.getFromNode = function () { var node = this; var fromColumn = -1, fromNode; for (var i = 0; i < node.linksTo.length; i++) { var point = node.linksTo[i]; if (point.fromNode.column > fromColumn && point.fromNode !== node // #16080 ) { fromNode = point.fromNode; fromColumn = fromNode.column; } } return { fromNode: fromNode, fromColumn: fromColumn }; }; /** * Calculate node.column if it's not set by user * @private */ SankeyPoint.prototype.setNodeColumn = function () { var node = this; if (!defined(node.options.column)) { // No links to this node, place it left if (node.linksTo.length === 0) { node.column = 0; } else { node.column = node.getFromNode().fromColumn + 1; } } }; /** * @private */ SankeyPoint.prototype.isValid = function () { return this.isNode || typeof this.weight === 'number'; }; return SankeyPoint; }(ColumnSeries.prototype.pointClass)); /* * * * Default Export * * */ return SankeyPoint; }); _registerModule(_modules, 'Series/Sankey/SankeyColumnComposition.js', [_modules['Core/Utilities.js']], function (U) { var defined = U.defined, relativeLength = U.relativeLength; var SankeyColumnComposition; (function (SankeyColumnComposition) { /* * * * Declarations * * */ /** * SankeyColumn Composition * @private * @function Highcharts.SankeyColumn#compose * * @param {Array<SankeyPoint>} points * The array of nodes * @param {SankeySeries} series * Series connected to column * @return {ArrayComposition} SankeyColumnArray */ function compose(points, series) { var sankeyColumnArray = points; sankeyColumnArray.sankeyColumn = new SankeyColumnAdditions(sankeyColumnArray, series); return sankeyColumnArray; } SankeyColumnComposition.compose = compose; /* * * * Classes * * */ var SankeyColumnAdditions = /** @class */ (function () { function SankeyColumnAdditions(points, series) { this.points = points; this.series = series; } /** * Calculate translation factor used in column and nodes distribution * @private * @function Highcharts.SankeyColumn#getTranslationFactor * * @param {SankeySeries} series * The Series * @return {number} TranslationFactor * Translation Factor */ SankeyColumnAdditions.prototype.getTranslationFactor = function (series) { var column = this.points, nodes = column.slice(), chart = series.chart, minLinkWidth = series.options.minLinkWidth || 0; var skipPoint, factor = 0, i, remainingHeight = ((chart.plotSizeY || 0) - (series.options.borderWidth || 0) - (column.length - 1) * series.nodePadding); // Because the minLinkWidth option doesn't obey the direct // translation, we need to run translation iteratively, check // node heights, remove those nodes affected by minLinkWidth, // check again, etc. while (column.length) { factor = remainingHeight / column.sankeyColumn.sum(); skipPoint = false; i = column.length; while (i--) { if (column[i].getSum() * factor < minLinkWidth) { column.splice(i, 1); remainingHeight -= minLinkWidth; skipPoint = true; } } if (!skipPoint) { break; } } // Re-insert original nodes column.length = 0; nodes.forEach(function (node) { column.push(node); }); return factor; }; /** * Get the top position of the column in pixels * @private * @function Highcharts.SankeyColumn#top * * @param {number} factor * The Translation Factor * @return {number} top * The top position of the column */ SankeyColumnAdditions.prototype.top = function (factor) { var series = this.series; var nodePadding = series.nodePadding; var height = this.points.reduce(function (height, node) { if (height > 0) { height += nodePadding; } var nodeHeight = Math.max(node.getSum() * factor, series.options.minLinkWidth || 0); height += nodeHeight; return height; }, 0); return ((series.chart.plotSizeY || 0) - height) / 2; }; /** * Get the left position of the column in pixels * @private * @function Highcharts.SankeyColumn#top * * @param {number} factor * The Translation Factor * @return {number} left * The left position of the column */ SankeyColumnAdditions.prototype.left = function (factor) { var series = this.series, chart = series.chart, equalNodes = series.options.equalNodes; var maxNodesLength = chart.inverted ? chart.plotHeight : chart.plotWidth, nodePadding = series.nodePadding; var width = this.points.reduce(function (width, node) { if (width > 0) { width += nodePadding; } var nodeWidth = equalNodes ? maxNodesLength / node.series.nodes.length - nodePadding : Math.max(node.getSum() * factor, series.options.minLinkWidth || 0); width += nodeWidth; return width; }, 0); return ((chart.plotSizeX || 0) - Math.round(width)) / 2; }; /** * Calculate sum of all nodes inside specific column * @private * @function Highcharts.SankeyColumn#sum * * @param {ArrayComposition} this * Sankey Column Array * * @return {number} sum * Sum of all nodes inside column */ SankeyColumnAdditions.prototype.sum = function () { return this.points.reduce(function (sum, node) { return sum + node.getSum(); }, 0); }; /** * Get the offset in pixels of a node inside the column * @private * @function Highcharts.SankeyColumn#offset * * @param {SankeyPoint} node * Sankey node * @param {number} factor * Translation Factor * @return {number} offset * Offset of a node inside column */ SankeyColumnAdditions.prototype.offset = function (node, factor) { var column = this.points, series = this.series, nodePadding = series.nodePadding; var offset = 0, totalNodeOffset; if (series.is('organization') && node.hangsFrom) { return { absoluteTop: node.hangsFrom.nodeY }; } for (var i = 0; i < column.length; i++) { var sum = column[i].getSum(); var height = Math.max(sum * factor, series.options.minLinkWidth || 0); var directionOffset = node.options[series.chart.inverted ? 'offsetHorizontal' : 'offsetVertical'], optionOffset = node.options.offset || 0; if (sum) { totalNodeOffset = height + nodePadding; } else { // If node sum equals 0 nodePadding is missed #12453 totalNodeOffset = 0; } if (column[i] === node) { return { relativeTop: offset + (defined(directionOffset) ? // directionOffset is a percent // of the node height relativeLength(directionOffset, height) : relativeLength(optionOffset, totalNodeOffset)) }; } offset += totalNodeOffset; } }; return SankeyColumnAdditions; }()); SankeyColumnComposition.SankeyColumnAdditions = SankeyColumnAdditions; })(SankeyColumnComposition || (SankeyColumnComposition = {})); return SankeyColumnComposition; }); _registerModule(_modules, 'Series/TreeUtilities.js', [_modules['Core/Color/Color.js'], _modules['Core/Utilities.js']], function (Color, U) { /* * * * (c) 2014-2021 Highsoft AS * * Authors: Jon Arild Nygard / Oystein Moseng * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var extend = U.extend, isArray = U.isArray, isNumber = U.isNumber, isObject = U.isObject, merge = U.merge, pick = U.pick; /* * * * Functions * * */ /* eslint-disable valid-jsdoc */ /** * @private */ function getColor(node, options) { var index = options.index, mapOptionsToLevel = options.mapOptionsToLevel, parentColor = options.parentColor, parentColorIndex = options.parentColorIndex, series = options.series, colors = options.colors, siblings = options.siblings, points = series.points, chartOptionsChart = series.chart.options.chart; var getColorByPoint, point, level, colorByPoint, colorIndexByPoint, color, colorIndex; /** * @private */ var variateColor = function (color) { var colorVariation = level && level.colorVariation; if (colorVariation && colorVariation.key === 'brightness' && index && siblings) { return Color.parse(color).brighten(colorVariation.to * (index / siblings)).get(); } return color; }; if (node) { point = points[node.i]; level = mapOptionsToLevel[node.level] || {}; getColorByPoint = point && level.colorByPoint; if (getColorByPoint) { colorIndexByPoint = point.index % (colors ? colors.length : chartOptionsChart.colorCount); colorByPoint = colors && colors[colorIndexByPoint]; } // Select either point color, level color or inherited color. if (!series.chart.styledMode) { color = pick(point && point.options.color, level && level.color, colorByPoint, parentColor && variateColor(parentColor), series.color); } colorIndex = pick(point && point.options.colorIndex, level && level.colorIndex, colorIndexByPoint, parentColorIndex, options.colorIndex); } return { color: color, colorIndex: colorIndex }; } /** * Creates a map from level number to its given options. * * @private * * @param {Object} params * Object containing parameters. * - `defaults` Object containing default options. The default options are * merged with the userOptions to get the final options for a specific * level. * - `from` The lowest level number. * - `levels` User options from series.levels. * - `to` The highest level number. * * @return {Highcharts.Dictionary<object>|null} * Returns a map from level number to its given options. */ function getLevelOptions(params) { var result = null, defaults, converted, i, from, to, levels; if (isObject(params)) { result = {}; from = isNumber(params.from) ? params.from : 1; levels = params.levels; converted = {}; defaults = isObject(params.defaults) ? params.defaults : {}; if (isArray(levels)) { converted = levels.reduce(function (obj, item) { var level, levelIsConstant, options; if (isObject(item) && isNumber(item.level)) { options = merge({}, item); levelIsConstant = pick(options.levelIsConstant, defaults.levelIsConstant); // Delete redundant properties. delete options.levelIsConstant; delete options.level; // Calculate which level these options apply to. level = item.level + (levelIsConstant ? 0 : from - 1); if (isObject(obj[level])) { merge(true, obj[level], options); // #16329 } else { obj[level] = options; } } return obj; }, {}); } to = isNumber(params.to) ? params.to : 1; for (i = 0; i <= to; i++) { result[i] = merge({}, defaults, isObject(converted[i]) ? converted[i] : {}); } } return result; } /** * @private * @todo Combine buildTree and buildNode with setTreeValues * @todo Remove logic from Treemap and make it utilize this mixin. */ function setTreeValues(tree, options) { var before = options.before, idRoot = options.idRoot, mapIdToNode = options.mapIdToNode, nodeRoot = mapIdToNode[idRoot], levelIsConstant = (options.levelIsConstant !== false), points = options.points, point = points[tree.i], optionsPoint = point && point.options || {}, children = []; var childrenTotal = 0; tree.levelDynamic = tree.level - (levelIsConstant ? 0 : nodeRoot.level); tree.name = pick(point && point.name, ''); tree.visible = (idRoot === tree.id || options.visible === true); if (typeof before === 'function') { tree = before(tree, options); } // First give the children some values tree.children.forEach(function (child, i) { var newOptions = extend({}, options); extend(newOptions, { index: i, siblings: tree.children.length, visible: tree.visible }); child = setTreeValues(child, newOptions); children.push(child); if (child.visible) { childrenTotal += child.val; } }); // Set the values var value = pick(optionsPoint.value, childrenTotal); tree.visible = value >= 0 && (childrenTotal > 0 || tree.visible); tree.children = children; tree.childrenTotal = childrenTotal; tree.isLeaf = tree.visible && !childrenTotal; tree.val = value; return tree; } /** * Update the rootId property on the series. Also makes sure that it is * accessible to exporting. * * @private * * @param {Object} series * The series to operate on. * * @return {string} * Returns the resulting rootId after update. */ function updateRootId(series) { var rootId, options; if (isObject(series)) { // Get the series options. options = isObject(series.options) ? series.options : {}; // Calculate the rootId. rootId = pick(series.rootNode, options.rootId, ''); // Set rootId on series.userOptions to pick it up in exporting. if (isObject(series.userOptions)) { series.userOptions.rootId = rootId; } // Set rootId on series to pick it up on next update. series.rootNode = rootId; } return rootId; } /* * * * Default Export * * */ var TreeUtilities = { getColor: getColor, getLevelOptions: getLevelOptions, setTreeValues: setTreeValues, updateRootId: updateRootId }; return TreeUtilities; }); _registerModule(_modules, 'Series/Sankey/SankeySeries.js', [_modules['Core/Color/Color.js'], _modules['Core/Globals.js'], _modules['Series/NodesComposition.js'], _modules['Series/Sankey/SankeyPoint.js'], _modules['Core/Series/SeriesRegistry.js'], _modules['Series/Sankey/SankeyColumnComposition.js'], _modules['Series/TreeUtilities.js'], _modules['Core/Utilities.js']], function (Color, H, NodesComposition, SankeyPoint, SeriesRegistry, SankeyColumnComposition, TU, U) { /* * * * Sankey diagram module * * (c) 2010-2021 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var Series = SeriesRegistry.series, ColumnSeries = SeriesRegistry.seriesTypes.column; var getLevelOptions = TU.getLevelOptions; var defined = U.defined, extend = U.extend, isObject = U.isObject, merge = U.merge, pick = U.pick, relativeLength = U.relativeLength, stableSort = U.stableSort; /* * * * Class * * */ /** * @private * @class * @name Highcharts.seriesTypes.sankey * * @augments Highcharts.Series */ var SankeySeries = /** @class */ (function (_super) { __extends(SankeySeries, _super); function SankeySeries() { /* * * * Static Properties * * */ var _this = _super !== null && _super.apply(this, arguments) || this; /* * * * Properties * * */ _this.colDistance = void 0; _this.data = void 0; _this.group = void 0; _this.nodeLookup = void 0; _this.nodePadding = void 0; _this.nodes = void 0; _this.nodeWidth = void 0; _this.options = void 0; _this.points = void 0; _this.translationFactor = void 0; return _this; /* eslint-enable valid-jsdoc */ } /* * * * Static Functions * * */ // eslint-disable-next-line valid-jsdoc /** * @private */ SankeySeries.getDLOptions = function (params) { var optionsPoint = (isObject(params.optionsPoint) ? params.optionsPoint.dataLabels : {}), optionsLevel = (isObject(params.level) ? params.level.dataLabels : {}), options = merge({ style: {} }, optionsLevel, optionsPoint); return options; }; /* * * * Functions * * */ /* eslint-disable valid-jsdoc */ /** * Create node columns by analyzing the nodes and the relations between * incoming and outgoing links. * @private */ SankeySeries.prototype.createNodeColumns = function () { var columns = []; this.nodes.forEach(function (node) { node.setNodeColumn(); if (!columns[node.column]) { columns[node.column] = SankeyColumnComposition.compose([], this); } columns[node.column].push(node); }, this); // Fill in empty columns (#8865) for (var i = 0; i < columns.length; i++) { if (typeof columns[i] === 'undefined') { columns[i] = SankeyColumnComposition.compose([], this); } } return columns; }; /** * Extend generatePoints by adding the nodes, which are Point objects * but pushed to the this.nodes array. * @private */ SankeySeries.prototype.generatePoints = function () { NodesComposition.generatePoints.apply(this, arguments); /** * Order the nodes, starting with the root node(s). (#9818) * @private */ function order(node, level) { // Prevents circular recursion: if (typeof node.level === 'undefined') { node.level = level; node.linksFrom.forEach(function (link) { if (link.toNode) { order(link.toNode, level + 1); } }); } } if (this.orderNodes) { this.nodes // Identify the root node(s) .filter(function (node) { return node.linksTo.length === 0; }) // Start by the root node(s) and recursively set the level // on all following nodes. .forEach(function (node) { order(node, 0); }); stableSort(this.nodes, function (a, b) { return a.level - b.level; }); } }; /** * Overridable function to get node padding, overridden in dependency * wheel series type. * @private */ SankeySeries.prototype.getNodePadding = function () { var nodePadding = this.options.nodePadding || 0; // If the number of columns is so great that they will overflow with // the given nodePadding, we sacrifice the padding in order to // render all nodes within the plot area (#11917). if (this.nodeColumns) { var maxLength = this.nodeColumns.reduce(function (acc, col) { return Math.max(acc, col.length); }, 0); if (maxLength * nodePadding > this.chart.plotSizeY) { nodePadding = this.chart.plotSizeY / maxLength; } } return nodePadding; }; /** * Define hasData function for non-cartesian series. * @private * @return {boolean} * Returns true if the series has points at all. */ SankeySeries.prototype.hasData = function () { return !!this.processedXData.length; // != 0 }; /** * Return the presentational attributes. * @private */ SankeySeries.prototype.pointAttribs = function (point, state) { if (!point) { return {}; } var series = this, level = point.isNode ? point.level : point.fromNode.level, levelOptions = series.mapOptionsToLevel[level || 0] || {}, options = point.options, stateOptions = (levelOptions.states && levelOptions.states[state || '']) || {}, values = [ 'colorByPoint', 'borderColor', 'borderWidth', 'linkOpacity' ].reduce(function (obj, key) { obj[key] = pick(stateOptions[key], options[key], levelOptions[key], series.options[key]); return obj; }, {}), color = pick(stateOptions.color, options.color, values.colorByPoint ? point.color : levelOptions.color); // Node attributes if (point.isNode) { return { fill: color, stroke: values.borderColor, 'stroke-width': values.borderWidth }; } // Link attributes return { fill: Color.parse(color).setOpacity(values.linkOpacity).get() }; }; /** * Extend the render function to also render this.nodes together with * the points. * @private */ SankeySeries.prototype.render = function () { var points = this.points; this.points = this.points.concat(this.nodes || []); ColumnSeries.prototype.render.call(this); this.points = points; }; /** * Run pre-translation by generating the nodeColumns. * @private */ SankeySeries.prototype.translate = function () { if (!this.processedXData) { this.processData(); } this.generatePoints(); this.nodeColumns = this.createNodeColumns(); this.nodeWidth = relativeLength(this.options.nodeWidth, this.chart.plotSizeX); var series = this, chart = this.chart, options = this.options, nodeWidth = this.nodeWidth, nodeColumns = this.nodeColumns; this.nodePadding = this.getNodePadding(); // Find out how much space is needed. Base it on the translation // factor of the most spaceous column. this.translationFactor = nodeColumns.reduce(function (translationFactor, column) { return Math.min(translationFactor, column.sankeyColumn.getTranslationFactor(series)); }, Infinity); this.colDistance = (chart.plotSizeX - nodeWidth - options.borderWidth) / Math.max(1, nodeColumns.length - 1); // Calculate level options used in sankey and organization series.mapOptionsToLevel = getLevelOptions({ // NOTE: if support for allowTraversingTree is added, then from // should be the level of the root node. from: 1, levels: options.levels, to: nodeColumns.length - 1, defaults: { borderColor: options.borderColor, borderRadius: options.borderRadius, borderWidth: options.borderWidth, color: series.color, colorByPoint: options.colorByPoint, // NOTE: if support for allowTraversingTree is added, then // levelIsConstant should be optional. levelIsConstant: true, linkColor: options.linkColor, linkLineWidth: options.linkLineWidth, linkOpacity: options.linkOpacity, states: options.states } }); // First translate all nodes so we can use them when drawing links nodeColumns.forEach(function (column) { column.forEach(function (node) { series.translateNode(node, column); }); }, this); // Then translate links