UNPKG

highcharts

Version:
270 lines (269 loc) 8.27 kB
/* * * * Networkgraph series * * (c) 2010-2025 Paweł Fus * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; /* * * * Class * * */ /** * The QuadTree node class. Used in Networkgraph chart as a base for Barnes-Hut * approximation. * * @private * @class * @name Highcharts.QuadTreeNode * * @param {Highcharts.Dictionary<number>} box * Available space for the node */ class QuadTreeNode { /* * * * Constructor * * */ constructor(box) { /* * * * Properties * * */ /** * Read only. If QuadTreeNode is an external node, Point is stored in * `this.body`. * * @name Highcharts.QuadTreeNode#body * @type {boolean|Highcharts.Point} */ this.body = false; /** * Read only. Internal nodes when created are empty to reserve the * space. If Point is added to this QuadTreeNode, QuadTreeNode is no * longer empty. * * @name Highcharts.QuadTreeNode#isEmpty * @type {boolean} */ this.isEmpty = false; /** * Read only. Flag to determine if QuadTreeNode is internal (and has * subnodes with mass and central position) or external (bound to * Point). * * @name Highcharts.QuadTreeNode#isInternal * @type {boolean} */ this.isInternal = false; /** * Read only. Array of subnodes. Empty if QuadTreeNode has just one * Point. When added another Point to this QuadTreeNode, array is * filled with four subnodes. * * @name Highcharts.QuadTreeNode#nodes * @type {Array<Highcharts.QuadTreeNode>} */ this.nodes = []; /** * Read only. The available space for node. * * @name Highcharts.QuadTreeNode#box * @type {Highcharts.Dictionary<number>} */ this.box = box; /** * Read only. The minium of width and height values. * * @name Highcharts.QuadTreeNode#boxSize * @type {number} */ this.boxSize = Math.min(box.width, box.height); } /* * * * Functions * * */ /** * When inserting another node into the box, that already hove one node, * divide the available space into another four quadrants. * * Indexes of quadrants are: * ``` * ------------- ------------- * | | | | | * | | | 0 | 1 | * | | divide() | | | * | 1 | -----------> ------------- * | | | | | * | | | 3 | 2 | * | | | | | * ------------- ------------- * ``` */ divideBox() { const halfWidth = this.box.width / 2, halfHeight = this.box.height / 2; // Top left this.nodes[0] = new QuadTreeNode({ left: this.box.left, top: this.box.top, width: halfWidth, height: halfHeight }); // Top right this.nodes[1] = new QuadTreeNode({ left: this.box.left + halfWidth, top: this.box.top, width: halfWidth, height: halfHeight }); // Bottom right this.nodes[2] = new QuadTreeNode({ left: this.box.left + halfWidth, top: this.box.top + halfHeight, width: halfWidth, height: halfHeight }); // Bottom left this.nodes[3] = new QuadTreeNode({ left: this.box.left, top: this.box.top + halfHeight, width: halfWidth, height: halfHeight }); } /** * Determine which of the quadrants should be used when placing node in * the QuadTree. Returned index is always in range `< 0 , 3 >`. * @private */ getBoxPosition(point) { const left = point.plotX < this.box.left + this.box.width / 2, top = point.plotY < this.box.top + this.box.height / 2; let index; if (left) { if (top) { // Top left index = 0; } else { // Bottom left index = 3; } } else { if (top) { // Top right index = 1; } else { // Bottom right index = 2; } } return index; } /** * Insert recursively point(node) into the QuadTree. If the given * quadrant is already occupied, divide it into smaller quadrants. * * @param {Highcharts.Point} point * Point/node to be inserted * @param {number} depth * Max depth of the QuadTree */ insert(point, depth) { let newQuadTreeNode; if (this.isInternal) { // Internal node: this.nodes[this.getBoxPosition(point)].insert(point, depth - 1); } else { this.isEmpty = false; if (!this.body) { // First body in a quadrant: this.isInternal = false; this.body = point; } else { if (depth) { // Every other body in a quadrant: this.isInternal = true; this.divideBox(); // Reinsert main body only once: if (this.body !== true) { this.nodes[this.getBoxPosition(this.body)] .insert(this.body, depth - 1); this.body = true; } // Add second body: this.nodes[this.getBoxPosition(point)] .insert(point, depth - 1); } else { // We are below max allowed depth. That means either: // - really huge number of points // - falling two points into exactly the same position // In this case, create another node in the QuadTree. // // Alternatively we could add some noise to the // position, but that could result in different // rendered chart in exporting. newQuadTreeNode = new QuadTreeNode({ top: point.plotX || NaN, left: point.plotY || NaN, // Width/height below 1px width: 0.1, height: 0.1 }); newQuadTreeNode.body = point; newQuadTreeNode.isInternal = false; this.nodes.push(newQuadTreeNode); } } } } /** * Each quad node requires it's mass and center position. That mass and * position is used to imitate real node in the layout by approximation. */ updateMassAndCenter() { let mass = 0, plotX = 0, plotY = 0; if (this.isInternal) { // Calculate weightened mass of the quad node: for (const pointMass of this.nodes) { if (!pointMass.isEmpty) { mass += pointMass.mass; plotX += pointMass.plotX * pointMass.mass; plotY += pointMass.plotY * pointMass.mass; } } plotX /= mass; plotY /= mass; } else if (this.body) { // Just one node, use coordinates directly: mass = this.body.mass; plotX = this.body.plotX; plotY = this.body.plotY; } // Store details: this.mass = mass; this.plotX = plotX; this.plotY = plotY; } } /* * * * Default Export * * */ export default QuadTreeNode;