client/client/modules/render/heatmap/renderer/dendrogram-renderer/binary-tree-graphics.js (422 lines of code) (raw):

import * as PIXI from 'pixi.js-legacy'; import {SubTreeNodeDisplayModes, getNodeDisplayMode} from './utilities/get-node-display-mode'; import HeatmapEventDispatcher from '../../utilities/heatmap-event-dispatcher'; import config from './config'; import getNodeIsCollapsed from './utilities/get-node-is-collapsed'; import getNodeSubTrees from './utilities/get-node-sub-trees'; import makeInitializable from '../../utilities/make-initializable'; class BinaryTreeGraphics extends HeatmapEventDispatcher { /** * * @param {SubTreeNode} tree * @param {HeatmapAxis} axis * @param {Vector} direction * @param {Vector} normal */ constructor(tree, axis, direction, normal) { super(); makeInitializable(this); this.visible = true; /** * Axis direction * @type {Vector} */ this.direction = direction; /** * Axis normal * @type {Vector} */ this.normal = normal; /** * Sub tree * @type {SubTreeNode} */ this.tree = tree; /** * Heatmap axis * @type {HeatmapAxis} */ this.axis = axis; /** * * @type {Map<SubTreeNode, BinaryTreeGraphics>} */ this.children = new Map(); this.container = new PIXI.Container(); } get totalSize() { if ( this.session && Number.isFinite(Number(this.session.levelSize)) && this.tree ) { return this.tree.level * this.session.levelSize; } return 0; } initialize(isCancelledFn) { this.destroyChildren(); this.container.removeChildren(); this.graphics = new PIXI.Graphics(); this.container.addChild(this.graphics); this.clearSession(); if (!isCancelledFn() && this.tree && this.axis) { const onInitialized = () => { let initialized = true; for (const child of this.children.values()) { initialized = initialized && child.initialized; } if (initialized) { this.hide(); } this.initialized = initialized; }; /** * * @param {SubTreeNode} node */ const iterate = (node) => { if (!isCancelledFn()) { if (node.leftSubTree) { const leftSubTreeGraphics = new BinaryTreeGraphics( node.leftSubTree, this.axis, this.direction, this.normal ); this.container.addChild(leftSubTreeGraphics.container); this.children.set(node.leftSubTree, leftSubTreeGraphics); } if (node.rightSubTree) { const rightSubTreeGraphics = new BinaryTreeGraphics( node.rightSubTree, this.axis, this.direction, this.normal ); this.container.addChild(rightSubTreeGraphics.container); this.children.set(node.rightSubTree, rightSubTreeGraphics); } if (node.left) { iterate(node.left); } if (node.right) { iterate(node.right); } } }; iterate(this.tree); onInitialized(); for (const child of this.children.values()) { child.onInitialized(onInitialized); child.initialize(isCancelledFn); } } else { this.initialized = true; } } destroyChildren() { if (this.children) { for (const child of this.children.values()) { child.destroy(); } this.children.clear(); } } destroy() { super.destroy(); this.tree = undefined; this.left = undefined; this.right = undefined; this.axis = undefined; this.destroyChildren(); this.children = undefined; if (this.container) { this.container.removeChildren(); } this.container = undefined; } clearSession() { this.session = {}; } getSessionFlags(levelSize) { if (this.axis) { const mode = getNodeDisplayMode(this.tree, this.axis); return { mode, modeChanged: this.session.mode !== mode, positionChanged: this.session.position !== this.axis.center, scaleChanged: this.session.scale !== this.axis.scale.tickSize, levelSizeChanged: this.session.levelSize !== levelSize }; } return { mode: SubTreeNodeDisplayModes.outsideViewport, modeChanged: false, positionChanged: false, scaleChanged: false, levelSizeChanged: false }; } updateSessionFlags(mode, levelSize) { if (this.axis) { this.session.mode = mode; this.session.position = this.axis.center; this.session.scale = this.axis.scale.tickSize; this.session.levelSize = levelSize; } } hide(clearSession = false) { if (!this.visible) { return; } // console.log(this.toString(), 'hiding'); this.visible = false; this.tree.level = 0; if (this.graphics) { this.graphics.clear(); } if (clearSession) { this.clearSession(); } if (this.children) { for (const child of this.children.values()) { child.hide(true); } } } getNodePosition(node = this.tree) { if (this.axis && node) { const {center} = node.indexRange; return this.axis.getCorrectedPositionWithinAxis(center + 0.5); } return 0; } getXCoordinate = (axisCoordinate, levelCoordinate) => this.axis && this.axis.scale && this.session ? Math.ceil( this.axis.scale.getDeviceDimension(axisCoordinate) * this.direction.x + levelCoordinate * this.normal.x * (this.session.levelSize || 0) ) : 0; getYCoordinate = (axisCoordinate, levelCoordinate) => this.axis && this.axis.scale && this.session ? Math.ceil( this.axis.scale.getDeviceDimension(axisCoordinate) * this.direction.y + levelCoordinate * this.normal.y * (this.session.levelSize || 0) ) : 0; renderLeafPoint(position, level = 0) { if (config.leafPoint) { const { fill = config.stroke, radius = 1 } = typeof config.leafPoint === 'object' ? config.leafPoint : {}; this.graphics .lineStyle(0, 0, 0) .beginFill(fill, 1) .drawCircle( this.getXCoordinate(position, level), this.getYCoordinate(position, level), radius ) .endFill(); } } renderNodeGraphics(options = {}) { const { level, left, right } = options; const levelSize = this.session.levelSize || 0; let radius = 0; let levelRadius = 0; let axisRadius = 0; if (config.edgeRadius > 0) { const distance = this.axis.scale.getDeviceDimension(Math.abs(left.position - right.position)); radius = Math.min(distance / 2.0, levelSize, config.edgeRadius); levelRadius = radius / levelSize; axisRadius = this.axis.scale.getScaleDimension(radius); } if (radius < 2) { radius = 0; } const leftAnchor = { x: this.getXCoordinate(left.position, level), y: this.getYCoordinate(left.position, level) }; const rightAnchor = { x: this.getXCoordinate(right.position, level), y: this.getYCoordinate(right.position, level) }; this.graphics .lineStyle(config.thickness, config.stroke, 1); if (left.visible) { this.graphics.moveTo( this.getXCoordinate(left.position, left.level), this.getYCoordinate(left.position, left.level) ); if (radius > 0) { this.graphics.lineTo( this.getXCoordinate(left.position, level - levelRadius), this.getYCoordinate(left.position, level - levelRadius), ); this.graphics.quadraticCurveTo( leftAnchor.x, leftAnchor.y, this.getXCoordinate(left.position + axisRadius, level), this.getYCoordinate(left.position + axisRadius, level) ); this.graphics.moveTo( this.getXCoordinate(left.position + axisRadius, level), this.getYCoordinate(left.position + axisRadius, level) ); } else { this.graphics.lineTo( leftAnchor.x, leftAnchor.y ); } } else { this.graphics.moveTo( this.getXCoordinate(left.position, level), this.getYCoordinate(left.position, level) ); } if (radius > 0) { this.graphics.lineTo( this.getXCoordinate(right.position - axisRadius, level), this.getYCoordinate(right.position - axisRadius, level) ); } else { this.graphics.lineTo( this.getXCoordinate(right.position, level), this.getYCoordinate(right.position, level) ); } if (right.visible) { if (radius > 0) { this.graphics.quadraticCurveTo( rightAnchor.x, rightAnchor.y, this.getXCoordinate(right.position, level - levelRadius), this.getYCoordinate(right.position, level - levelRadius) ); this.graphics.moveTo( this.getXCoordinate(right.position, level - levelRadius), this.getYCoordinate(right.position, level - levelRadius) ); } this.graphics.lineTo( this.getXCoordinate(right.position, right.level), this.getYCoordinate(right.position, right.level) ); } } /** * * @param {Object} [options] * @param {number} [options.levelSize=5] * @returns {boolean} */ renderTree(options = {}) { const { levelSize = 5 } = options; const displayMode = getNodeDisplayMode(this.tree, this.axis); if ( displayMode === SubTreeNodeDisplayModes.outsideViewport || getNodeIsCollapsed(this.tree, this.axis) ) { this.hide(); return true; } this.graphics.clear(); /** * * @param {SubTreeNode} node * @returns {BinaryTreeGraphics} */ const getChildRenderer = (node) => { if (node && this.children.has(node)) { return this.children.get(node); } return undefined; }; /** * * @param {SubTreeNode} node * @returns {{level: number, position: number}} */ const renderNode = (node) => { const mode = getNodeDisplayMode(node, this.axis); const collapsed = getNodeIsCollapsed(node, this.axis); const position = this.getNodePosition(node); if ( mode === SubTreeNodeDisplayModes.outsideViewport || collapsed ) { const subTrees = getNodeSubTrees(node); for (const subTree of subTrees) { const renderer = getChildRenderer(subTree); if (renderer) { renderer.hide(true); } } if (mode !== SubTreeNodeDisplayModes.outsideViewport) { this.renderLeafPoint(position); } return { level: 0, position }; } if (node.isLeaf && !node.hasSubTree) { this.renderLeafPoint(this.getNodePosition(node)); return { level: 0, position }; } const left = { level: 0, position: 0, visible: true, isLeaf: false }; const right = { level: 0, position: 0, visible: true, isLeaf: false }; if ( node.isLeaf && node.hasSubTree && node.leftSubTree && node.rightSubTree ) { const leftRenderer = getChildRenderer(node.leftSubTree); const rightRenderer = getChildRenderer(node.rightSubTree); if (leftRenderer && rightRenderer) { leftRenderer.render(options); rightRenderer.render(options); } left.level = node.leftSubTree.level; right.level = node.rightSubTree.level; left.position = leftRenderer.getNodePosition(); right.position = rightRenderer.getNodePosition(); left.visible = getNodeDisplayMode(node.leftSubTree, this.axis) !== SubTreeNodeDisplayModes.outsideViewport; right.visible = getNodeDisplayMode(node.rightSubTree, this.axis) !== SubTreeNodeDisplayModes.outsideViewport; } else { // not a leaf and does not have sub trees const {level: lLevel, position: lPosition} = renderNode(node.left); const {level: rLevel, position: rPosition} = renderNode(node.right); left.level = lLevel; left.position = lPosition; left.visible = getNodeDisplayMode(node.left, this.axis) !== SubTreeNodeDisplayModes.outsideViewport; right.level = rLevel; right.position = rPosition; right.visible = getNodeDisplayMode(node.right, this.axis) !== SubTreeNodeDisplayModes.outsideViewport; } const level = Math.max( -1, left.level, right.level ) + 1; this.renderNodeGraphics({ level, levelSize, left, right }); return { level, position: this.axis.getCorrectedPositionWithinAxis( (left.position + right.position) / 2.0 ) }; }; const {level} = renderNode(this.tree); this.tree.level = level; this.visible = true; } /** * * @param {Object} [options] * @param {number} [options.levelSize=5] * @returns {boolean} */ render(options = {}) { if (!this.initialized) { return false; } const { levelSize = 5 } = options; const { scaleChanged, positionChanged, mode, modeChanged } = this.getSessionFlags(levelSize); this.updateSessionFlags(mode, levelSize); if ( modeChanged || scaleChanged || (mode === SubTreeNodeDisplayModes.intersectsViewport && positionChanged) ) { // console.log(this.toString(), 'render. axis:', this.axis.start.toFixed(2), this.axis.end.toFixed(2), 'mode:', mode); return this.renderTree(options); } else if (positionChanged) { // console.log(this.toString(), 'nothing changed'); } return false; } toString() { return `${this.constructor.name} (${this.tree.indexRange.start} - ${this.tree.indexRange.end})`; } } export default BinaryTreeGraphics;