UNPKG

highcharts

Version:
377 lines (376 loc) 12.7 kB
/* * * * Networkgraph series * * (c) 2010-2025 Paweł Fus * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import SVGElement from '../../Core/Renderer/SVG/SVGElement.js'; import DragNodesComposition from '../DragNodesComposition.js'; import GraphLayout from '../GraphLayoutComposition.js'; import H from '../../Core/Globals.js'; const { noop } = H; import NetworkgraphPoint from './NetworkgraphPoint.js'; import NetworkgraphSeriesDefaults from './NetworkgraphSeriesDefaults.js'; import NodesComposition from '../NodesComposition.js'; import ReingoldFruchtermanLayout from './ReingoldFruchtermanLayout.js'; import SeriesRegistry from '../../Core/Series/SeriesRegistry.js'; const { series: Series, seriesTypes: { column: { prototype: columnProto }, line: { prototype: lineProto } } } = SeriesRegistry; import D from '../SimulationSeriesUtilities.js'; const { initDataLabels, initDataLabelsDefer } = D; import U from '../../Core/Utilities.js'; const { addEvent, defined, extend, merge, pick } = U; import TextPath from '../../Extensions/TextPath.js'; TextPath.compose(SVGElement); /* * * * Class * * */ /** * @private * @class * @name Highcharts.seriesTypes.networkgraph * * @extends Highcharts.Series */ class NetworkgraphSeries extends Series { constructor() { /* * * * Static Properties * * */ super(...arguments); this.deferDataLabels = true; } /* * * * Static Functions * * */ static compose(ChartClass) { DragNodesComposition.compose(ChartClass); ReingoldFruchtermanLayout.compose(ChartClass); } /* * * * Functions * * */ /** * Defer the layout. * Each series first registers all nodes and links, then layout * calculates all nodes positions and calls `series.render()` in every * simulation step. * * Note: * Animation is done through `requestAnimationFrame` directly, without * `Highcharts.animate()` use. * @private */ deferLayout() { const layoutOptions = this.options.layoutAlgorithm, chartOptions = this.chart.options.chart; let layout, graphLayoutsStorage = this.chart.graphLayoutsStorage, graphLayoutsLookup = this.chart.graphLayoutsLookup; if (!this.visible) { return; } if (!graphLayoutsStorage) { this.chart.graphLayoutsStorage = graphLayoutsStorage = {}; this.chart.graphLayoutsLookup = graphLayoutsLookup = []; } layout = graphLayoutsStorage[layoutOptions.type]; if (!layout) { layoutOptions.enableSimulation = !defined(chartOptions.forExport) ? layoutOptions.enableSimulation : !chartOptions.forExport; graphLayoutsStorage[layoutOptions.type] = layout = new GraphLayout.layouts[layoutOptions.type](); layout.init(layoutOptions); graphLayoutsLookup.splice(layout.index, 0, layout); } this.layout = layout; layout.setArea(0, 0, this.chart.plotWidth, this.chart.plotHeight); layout.addElementsToCollection([this], layout.series); layout.addElementsToCollection(this.nodes, layout.nodes); layout.addElementsToCollection(this.points, layout.links); } /** * @private */ destroy() { if (this.layout) { this.layout.removeElementFromCollection(this, this.layout.series); } NodesComposition.destroy.call(this); } /** * Networkgraph has two separate collections of nodes and lines, render * dataLabels for both sets: * @private */ drawDataLabels() { // We defer drawing the dataLabels // until dataLabels.animation.defer time passes if (this.deferDataLabels) { return; } const dlOptions = this.options.dataLabels; let textPath; if (dlOptions?.textPath) { textPath = dlOptions.textPath; } // Render node labels: Series.prototype.drawDataLabels.call(this, this.nodes); // Render link labels: if (dlOptions?.linkTextPath) { // If linkTextPath is set, render link labels with linkTextPath dlOptions.textPath = dlOptions.linkTextPath; } Series.prototype.drawDataLabels.call(this, this.data); // Go back to textPath for nodes if (dlOptions?.textPath) { dlOptions.textPath = textPath; } } /** * Extend generatePoints by adding the nodes, which are Point objects * but pushed to the this.nodes array. * @private */ generatePoints() { let node, i; NodesComposition.generatePoints.apply(this, arguments); // In networkgraph, it's fine to define standalone nodes, create // them: if (this.options.nodes) { this.options.nodes.forEach(function (nodeOptions) { if (!this.nodeLookup[nodeOptions.id]) { this.nodeLookup[nodeOptions.id] = this.createNode(nodeOptions.id); } }, this); } for (i = this.nodes.length - 1; i >= 0; i--) { node = this.nodes[i]; node.degree = node.getDegree(); node.radius = pick(node.marker && node.marker.radius, this.options.marker && this.options.marker.radius, 0); node.key = node.name; // If node exists, but it's not available in nodeLookup, // then it's leftover from previous runs (e.g. setData) if (!this.nodeLookup[node.id]) { node.remove(); } } this.data.forEach(function (link) { link.formatPrefix = 'link'; }); this.indexateNodes(); } /** * In networkgraph, series.points refers to links, * but series.nodes refers to actual points. * @private */ getPointsCollection() { return this.nodes || []; } /** * Set index for each node. Required for proper `node.update()`. * Note that links are indexated out of the box in `generatePoints()`. * * @private */ indexateNodes() { this.nodes.forEach(function (node, index) { node.index = index; }); } /** * Extend init with base event, which should stop simulation during * update. After data is updated, `chart.render` resumes the simulation. * @private */ init(chart, options) { super.init(chart, options); initDataLabelsDefer.call(this); addEvent(this, 'updatedData', () => { if (this.layout) { this.layout.stop(); } }); addEvent(this, 'afterUpdate', () => { this.nodes.forEach((node) => { if (node && node.series) { node.resolveColor(); } }); }); // If the dataLabels.animation.defer time is longer than // the time it takes for the layout to become stable then // drawDataLabels would never be called (that's why we force it here) addEvent(this, 'afterSimulation', function () { this.deferDataLabels = false; this.drawDataLabels(); }); return this; } /** * Extend the default marker attribs by using a non-rounded X position, * otherwise the nodes will jump from pixel to pixel which looks a bit * jaggy when approaching equilibrium. * @private */ markerAttribs(point, state) { const attribs = Series.prototype.markerAttribs.call(this, point, state); // Series.render() is called before initial positions are set: if (!defined(point.plotY)) { attribs.y = 0; } attribs.x = (point.plotX || 0) - (attribs.width || 0) / 2; return attribs; } /** * Return the presentational attributes. * @private */ pointAttribs(point, state) { // By default, only `selected` state is passed on const pointState = state || point && point.state || 'normal', stateOptions = this.options.states[pointState]; let attribs = Series.prototype.pointAttribs.call(this, point, pointState); if (point && !point.isNode) { attribs = point.getLinkAttributes(); // For link, get prefixed names: if (stateOptions) { attribs = { // TO DO: API? stroke: stateOptions.linkColor || attribs.stroke, dashstyle: (stateOptions.linkDashStyle || attribs.dashstyle), opacity: pick(stateOptions.linkOpacity, attribs.opacity), 'stroke-width': stateOptions.linkColor || attribs['stroke-width'] }; } } return attribs; } /** * Extend the render function to also render this.nodes together with * the points. * @private */ render() { const series = this, points = series.points, hoverPoint = series.chart.hoverPoint, dataLabels = []; // Render markers: series.points = series.nodes; lineProto.render.call(this); series.points = points; points.forEach(function (point) { if (point.fromNode && point.toNode) { point.renderLink(); point.redrawLink(); } }); if (hoverPoint && hoverPoint.series === series) { series.redrawHalo(hoverPoint); } if (series.chart.hasRendered && !series.options.dataLabels.allowOverlap) { series.nodes.concat(series.points).forEach(function (node) { if (node.dataLabel) { dataLabels.push(node.dataLabel); } }); series.chart.hideOverlappingLabels(dataLabels); } } /** * When state should be passed down to all points, concat nodes and * links and apply this state to all of them. * @private */ setState(state, inherit) { if (inherit) { this.points = this.nodes.concat(this.data); Series.prototype.setState.apply(this, arguments); this.points = this.data; } else { Series.prototype.setState.apply(this, arguments); } // If simulation is done, re-render points with new states: if (!this.layout.simulation && !state) { this.render(); } } /** * Run pre-translation and register nodes&links to the deffered layout. * @private */ translate() { 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; }); }); } } NetworkgraphSeries.defaultOptions = merge(Series.defaultOptions, NetworkgraphSeriesDefaults); extend(NetworkgraphSeries.prototype, { pointClass: NetworkgraphPoint, animate: void 0, // Animation is run in `series.simulation` directTouch: true, drawGraph: void 0, forces: ['barycenter', 'repulsive', 'attractive'], hasDraggableNodes: true, isCartesian: false, noSharedTooltip: true, pointArrayMap: ['from', 'to'], requireSorting: false, trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], initDataLabels: initDataLabels, buildKDTree: noop, createNode: NodesComposition.createNode, drawTracker: columnProto.drawTracker, onMouseDown: DragNodesComposition.onMouseDown, onMouseMove: DragNodesComposition.onMouseMove, onMouseUp: DragNodesComposition.onMouseUp, redrawHalo: DragNodesComposition.redrawHalo }); SeriesRegistry.registerSeriesType('networkgraph', NetworkgraphSeries); /* * * * Default Export * * */ export default NetworkgraphSeries; /* * * * API Declarations * * */ /** * Callback that fires after the end of Networkgraph series simulation * when the layout is stable. * * @callback Highcharts.NetworkgraphAfterSimulationCallbackFunction * * @param {Highcharts.Series} this * The series where the event occurred. * * @param {global.Event} event * The event that occurred. */ ''; // Detach doclets above