/** * Networkgraph series * * (c) 2010-2019 Paweł Fus * * License: www.highcharts.com/license */ 'use strict'; import H from '../../parts/Globals.js'; import '../../parts/Utilities.js'; import '../../parts/Options.js'; import '../../mixins/nodes.js'; import '/layouts.js'; var addEvent = H.addEvent, defined = H.defined, seriesType = H.seriesType, seriesTypes = H.seriesTypes, pick = H.pick, Chart = H.Chart, Point = H.Point, Series = H.Series; /** * A networkgraph is a type of relationship chart, where connnections * (links) attracts nodes (points) and other nodes repulse each other. * * @extends plotOptions.line * @product highcharts * @sample highcharts/demo/network-graph/ * Networkgraph * @since 7.0.0 * @excluding boostThreshold, animation, animationLimit, connectEnds, * connectNulls, dragDrop, getExtremesFromAll, label, linecap, * negativeColor, pointInterval, pointIntervalUnit, * pointPlacement, pointStart, softThreshold, stack, stacking, * step, threshold, xAxis, yAxis, zoneAxis * @optionparent plotOptions.networkgraph */ seriesType('networkgraph', 'line', { marker: { enabled: true }, dataLabels: { format: '{key}' }, /** * Link style options */ link: { /** * A name for the dash style to use for links. * * @type {String} * @apioption plotOptions.networkgraph.link.dashStyle * @defaults undefined */ /** * Color of the link between two nodes. */ color: 'rgba(100, 100, 100, 0.5)', /** * Width (px) of the link between two nodes. */ width: 1 }, /** * Flag to determine if nodes are draggable or not. */ draggable: true, layoutAlgorithm: { /** * Ideal length (px) of the link between two nodes. When not defined, * length is calculated as: * `Math.pow(availableWidth * availableHeight / nodesLength, 0.4);` * * Note: Because of the algorithm specification, length of each link * might be not exactly as specified. * * @type {number} * @apioption series.networkgraph.layoutAlgorithm.linkLength * @sample highcharts/series-networkgraph/styled-links/ * Numerical values * @defaults undefined */ /** * Initial layout algorithm for positioning nodes. Can be one of * built-in options ("circle", "random") or a function where positions * should be set on each node (`this.nodes`) as `node.plotX` and * `node.plotY` * * @sample highcharts/series-networkgraph/initial-positions/ * Initial positions with callback * @type {String|Function} * @validvalue ["circle", "random"] */ initialPositions: 'circle', /** * Experimental. Enables live simulation of the algorithm * implementation. All nodes are animated as the forces applies on * them. * * @sample highcharts/demo/network-graph/ * Live simulation enabled */ enableSimulation: false, /** * Type of the algorithm used when positioning nodes. * * @validvalue ["reingold-fruchterman"] */ type: 'reingold-fruchterman', /** * Max number of iterations before algorithm will stop. In general, * algorithm should find positions sooner, but when rendering huge * number of nodes, it is recommended to increase this value as * finding perfect graph positions can require more time. */ maxIterations: 1000, /** * Gravitational const used in the barycenter force of the algorithm. * * @sample highcharts/series-networkgraph/forces/ * Custom forces */ gravitationalConstant: 0.0625, /** * Friction applied on forces to prevent nodes rushing to fast to the * desired positions. */ friction: -0.981, /** * Repulsive force applied on a node. Passed are two arguments: * - `d` - which is current distance between two nodes * - `k` - which is desired distance between two nodes * * @sample highcharts/series-networkgraph/forces/ * Custom forces * @type {Function} * @default function (d, k) { return k * k / d; } */ repulsiveForce: function (d, k) { /* basic, not recommended: return k / d; */ /* standard: return k * k / d; */ /* grid-variant: return k * k / d * (2 * k - d > 0 ? 1 : 0); */ return k * k / d; }, /** * Attraction force applied on a node which is conected to another node * by a link. Passed are two arguments: * - `d` - which is current distance between two nodes * - `k` - which is desired distance between two nodes * * @sample highcharts/series-networkgraph/forces/ * Custom forces * @type {Function} * @default function (d, k) { return k * k / d; } */ attractiveForce: function (d, k) { /* basic, not recommended: return d / k; */ return d * d / k; } }, showInLegend: false }, { isNetworkgraph: true, drawGraph: null, isCartesian: false, requireSorting: false, directTouch: true, noSharedTooltip: true, trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], drawTracker: H.TrackerMixin.drawTrackerPoint, // Animation is run in `series.simulation`. animate: null, /** * Create a single node that holds information on incoming and outgoing * links. */ createNode: H.NodesMixin.createNode, /** * Extend generatePoints by adding the nodes, which are Point objects * but pushed to the this.nodes array. */ generatePoints: function () { var nodeLookup = {}, chart = this.chart; H.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; }); // 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); if (this.options.nodes) { this.options.nodes.forEach( function (nodeOptions) { if (!nodeLookup[nodeOptions.id]) { nodeLookup[nodeOptions.id] = this .createNode(nodeOptions.id); } }, this ); } }, /** * Run pre-translation by generating the nodeColumns. */ translate: function () { if (!this.processedXData) { this.processData(); } this.generatePoints(); this.deferLayout(); this.nodes.forEach(function (node) { // Draw the links from this node node.isInside = true; node.linksFrom.forEach(function (point) { point.shapeType = 'path'; // Pass test in drawPoints point.y = 1; }); }); }, deferLayout: function () { var layoutOptions = this.options.layoutAlgorithm, graphLayoutsStorage = this.chart.graphLayoutsStorage, chartOptions = this.chart.options.chart, layout; if (!this.visible) { return; } if (!graphLayoutsStorage) { this.chart.graphLayoutsStorage = graphLayoutsStorage = {}; } layout = graphLayoutsStorage[layoutOptions.type]; if (!layout) { layoutOptions.enableSimulation = !defined(chartOptions.forExport) ? layoutOptions.enableSimulation : !chartOptions.forExport; graphLayoutsStorage[layoutOptions.type] = layout = new H.layouts[layoutOptions.type](layoutOptions); } this.layout = layout; layout.setArea(0, 0, this.chart.plotWidth, this.chart.plotHeight); layout.addSeries(this); layout.addNodes(this.nodes); layout.addLinks(this.points); }, /** * Extend the render function to also render this.nodes together with * the points. */ render: function () { var points = this.points, hoverPoint = this.chart.hoverPoint, dataLabels = []; // Render markers: this.points = this.nodes; seriesTypes.line.prototype.render.call(this); this.points = points; points.forEach(function (point) { if (point.fromNode && point.toNode) { point.renderLink(); point.redrawLink(); } }); if (hoverPoint && hoverPoint.series === this) { this.redrawHalo(hoverPoint); } this.nodes.forEach(function (node) { if (node.dataLabel) { dataLabels.push(node.dataLabel); } }); H.Chart.prototype.hideOverlappingLabels(dataLabels); }, /* * Draggable mode: */ redrawHalo: function (point) { if (point && this.halo) { this.halo.attr({ d: point.haloPath( this.options.states.hover.halo.size ) }); } }, onMouseDown: function (point, event) { var normalizedEvent = this.chart.pointer.normalize(event); point.fixedPosition = { chartX: normalizedEvent.chartX, chartY: normalizedEvent.chartY, plotX: point.plotX, plotY: point.plotY }; }, onMouseMove: function (point, event) { if (point.fixedPosition) { var series = this, chart = series.chart, normalizedEvent = chart.pointer.normalize(event), diffX = point.fixedPosition.chartX - normalizedEvent.chartX, diffY = point.fixedPosition.chartY - normalizedEvent.chartY, newPlotX, newPlotY; // At least 5px to apply change (avoids simple click): if (Math.abs(diffX) > 5 || Math.abs(diffY) > 5) { newPlotX = point.fixedPosition.plotX - diffX; newPlotY = point.fixedPosition.plotY - diffY; if (chart.isInsidePlot(newPlotX, newPlotY)) { point.plotX = newPlotX; point.plotY = newPlotY; series.redrawHalo(); if (!series.layout.simulation) { // Start new simulation: if (!series.layout.enableSimulation) { // Run only one iteration to speed things up: series.layout.setMaxIterations(1); } // When dragging nodes, we don't need to calculate // initial positions and rendering nodes: series.layout.setInitialRendering(false); series.layout.run(); // Restore defaults: series.layout.setInitialRendering(true); } else { // Extend current simulation: series.layout.resetSimulation(); } } } } }, onMouseUp: function (point) { if (point.fixedPosition) { this.layout.run(); delete point.fixedPosition; } }, destroy: function () { this.nodes.forEach(function (node) { node.destroy(); }); return Series.prototype.destroy.apply(this, arguments); } }, { getDegree: function () { var deg = this.isNode ? this.linksFrom.length + this.linksTo.length : 0; return deg === 0 ? 1 : deg; }, // Links: getLinkAttribues: function () { var linkOptions = this.series.options.link, pointOptions = this.options; return { 'stroke-width': pick(pointOptions.width, linkOptions.width), stroke: pointOptions.color || linkOptions.color, dashstyle: pointOptions.dashStyle || linkOptions.dashStyle }; }, renderLink: function () { if (!this.graphic) { this.graphic = this.series.chart.renderer .path( this.getLinkPath(this.fromNode, this.toNode) ) .attr(this.getLinkAttribues()) .add(this.series.group); } }, redrawLink: function () { if (this.graphic) { this.graphic.animate({ d: this.getLinkPath(this.fromNode, this.toNode) }); } }, getLinkPath: function (from, to) { return [ 'M', from.plotX, from.plotY, 'L', to.plotX, to.plotY ]; /* IDEA: different link shapes? return [ 'M', from.plotX, from.plotY, 'Q', (to.plotX + from.plotX) / 2, (to.plotY + from.plotY) / 2 + 15, to.plotX, to.plotY ];*/ }, // Default utils: destroy: function () { if (this.isNode) { this.linksFrom.forEach( function (linkFrom) { if (linkFrom.graphic) { linkFrom.graphic = linkFrom.graphic.destroy(); } } ); } return Point.prototype.destroy.apply(this, arguments); } }); addEvent(seriesTypes.networkgraph, 'updatedData', function () { if (this.layout) { this.layout.stop(); } }); addEvent(seriesTypes.networkgraph.prototype.pointClass, 'remove', function () { if (this.series.layout) { if (this.isNode) { this.series.layout.removeNode(this); } else { this.series.layout.removeLink(this); } } }); /* * Multiple series support: */ // Clear previous layouts addEvent(Chart, 'predraw', function () { if (this.graphLayoutsStorage) { H.objectEach( this.graphLayoutsStorage, function (layout) { layout.stop(); } ); } }); addEvent(Chart, 'render', function () { if (this.graphLayoutsStorage) { H.setAnimation(false, this); H.objectEach( this.graphLayoutsStorage, function (layout) { layout.run(); } ); } }); /* * Draggable mode: */ addEvent( seriesTypes.networkgraph.prototype.pointClass, 'mouseOver', function () { H.css(this.series.chart.container, { cursor: 'move' }); } ); addEvent( seriesTypes.networkgraph.prototype.pointClass, 'mouseOut', function () { H.css(this.series.chart.container, { cursor: 'default' }); } ); addEvent( Chart, 'load', function () { var chart = this, unbinders = []; if (chart.container) { unbinders.push( addEvent( chart.container, 'mousedown', function (event) { var point = chart.hoverPoint; if ( point && point.series && point.series.isNetworkgraph && point.series.options.draggable ) { point.series.onMouseDown(point, event); unbinders.push(addEvent( chart.container, 'mousemove', function (e) { return point.series.onMouseMove(point, e); } )); unbinders.push(addEvent( chart.container.ownerDocument, 'mouseup', function (e) { return point.series.onMouseUp(point, e); } )); } } ) ); } addEvent(chart, 'destroy', function () { unbinders.forEach(function (unbind) { unbind(); }); }); } ); /** * A `networkgraph` series. If the [type](#series.networkgraph.type) option is * not specified, it is inherited from [chart.type](#chart.type). * * @type {Object} * @extends series,plotOptions.networkgraph * @excluding boostThreshold, animation, animationLimit, connectEnds, * connectNulls, dragDrop, getExtremesFromAll, label, linecap, * negativeColor, pointInterval, pointIntervalUnit, * pointPlacement, pointStart, softThreshold, stack, stacking, * step, threshold, xAxis, yAxis, zoneAxis * @product highcharts * @apioption series.networkgraph */ /** * An array of data points for the series. For the `networkgraph` series type, * points can be given in the following way: * * An array of objects with named values. The following snippet shows only a * few settings, see the complete options set below. If the total number of * data points exceeds the series' * [turboThreshold](#series.area.turboThreshold), this option is not available. * * ```js * data: [{ * from: 'Category1', * to: 'Category2' * }, { * from: 'Category1', * to: 'Category3' * }] * ``` * * @type {Array} * @extends series.line.data * @excluding drilldown,marker,x,y,draDrop * @sample {highcharts} highcharts/chart/reflow-true/ * Numerical values * @sample {highcharts} highcharts/series/data-array-of-arrays/ * Arrays of numeric x and y * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ * Arrays of datetime x and y * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ * Config objects * @product highcharts * @apioption series.networkgraph.data */ /** * The node that the link runs from. * * @type {String} * @product highcharts * @apioption series.networkgraph.data.from */ /** * The node that the link runs to. * * @type {String} * @product highcharts * @apioption series.networkgraph.data.to */ /** * The weight of the link. * * @type {Number} * @product highcharts * @apioption series.networkgraph.data.weight */ /** * A collection of options for the individual nodes. The nodes in a * networkgraph diagram are auto-generated instances of `Highcharts.Point`, * but options can be applied here and linked by the `id`. * * @sample highcharts/series-networkgraph/data-options/ * Networkgraph diagram with node options * * @type {Array<*>} * @product highcharts * @apioption series.networkgraph.nodes */ /** * The id of the auto-generated node, refering to the `from` or `to` setting of * the link. * * @type {string} * @product highcharts * @apioption series.networkgraph.nodes.id */ /** * The color of the auto generated node. * * @type {Highcharts.ColorString} * @product highcharts * @apioption series.networkgraph.nodes.color */ /** * The color index of the auto generated node, especially for use in styled * mode. * * @type {number} * @product highcharts * @apioption series.networkgraph.nodes.colorIndex */ /** * The name to display for the node in data labels and tooltips. Use this when * the name is different from the `id`. Where the id must be unique for each * node, this is not necessary for the name. * * @sample highcharts/series-networkgraph/data-options/ * Networkgraph diagram with node options * * @type {string} * @product highcharts * @apioption series.networkgraph.nodes.name */