import { HierarchyPointNode } from '@vx/hierarchy/lib/types'; import React, { useEffect, useMemo, useState } from 'react'; import { Group } from '@vx/group'; import { LinkHorizontal } from '@vx/shape'; import { NodeData, Direction, EntitySelectParams, TreeProps } from './types'; import LineageEntityNode from './LineageEntityNode'; import adjustVXTreeLayout from './utils/adjustVXTreeLayout'; type Props = { tree: HierarchyPointNode; zoom: { transformMatrix: { scaleX: number; scaleY: number; translateX: number; translateY: number; skewX: number; skewY: number; }; }; canvasHeight: number; onEntityClick: (EntitySelectParams) => void; onLineageExpand: (LineageExpandParams) => void; selectedEntity?: EntitySelectParams; margin: TreeProps['margin']; direction: Direction; debouncedSetYCanvasScale: (number) => void; yCanvasScale: number; xCanvasScale: number; }; function findMin(arr) { if (!arr) return Infinity; if (arr.length < 2) return Infinity; arr.sort((a, b) => { return a - b; }); let min = arr[1] - arr[0]; const n = arr.length; for (let i = 0; i < n - 1; i++) { const m = arr[i + 1] - arr[i]; if (m < min && m > 0) { min = m; } } if (min === 0) return Infinity; return min; // minimum difference. } function transformToString(transform: { scaleX: number; scaleY: number; translateX: number; translateY: number; skewX: number; skewY: number; }): string { return `matrix(${transform.scaleX}, ${transform.skewX}, ${transform.skewY}, ${transform.scaleY}, ${transform.translateX}, ${transform.translateY})`; } export default function LineageTreeNodeAndEdgeRenderer({ tree, zoom, margin, canvasHeight, onEntityClick, onLineageExpand, selectedEntity, direction, debouncedSetYCanvasScale, yCanvasScale, xCanvasScale, }: Props) { const [hoveredEntity, setHoveredEntity] = useState(undefined); const { nodesToRender, edgesToRender } = useMemo(() => { return adjustVXTreeLayout({ tree, direction }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [tree, direction, xCanvasScale, yCanvasScale]); useEffect(() => { const nodesByDepth: { [x: number]: { x: number; y: number; data: Omit }[] } = {}; nodesToRender.forEach((descendent) => { // we need to track clustering of nodes so we can expand the canvas horizontally nodesByDepth[descendent.y] = [...(nodesByDepth[descendent.y] || []), descendent]; }); Object.keys(nodesByDepth).forEach((depth) => { if (findMin(nodesByDepth[depth]?.map((entity) => entity.x)) < 90) { debouncedSetYCanvasScale(yCanvasScale * 1.025); } }); }, [nodesToRender, debouncedSetYCanvasScale, yCanvasScale, xCanvasScale]); // the layout does not always center the root node. To reverse this affect, we need to determine how far off // the root node is from center and re-adjust from there const alteredTransform = { ...zoom.transformMatrix }; alteredTransform.translateY -= (tree.x - canvasHeight / 2 - 125) * alteredTransform.scaleX; const renderedEdges = new Set(); const renderedNodes = new Set(); return ( {edgesToRender.map((link) => { if (renderedEdges.has(`edge-${link.source.data.urn}-${link.target.data.urn}-${direction}`)) { return null; } renderedEdges.add(`edge-${link.source.data.urn}-${link.target.data.urn}-${direction}`); return ( ); })} {nodesToRender.map((node) => { if (renderedNodes.has(`node-${node.data.urn}-${direction}`)) { return null; } renderedNodes.add(`node-${node.data.urn}-${direction}`); const isSelected = node.data.urn === selectedEntity?.urn; const isHovered = node.data.urn === hoveredEntity?.urn; return ( setHoveredEntity(select)} onEntityClick={onEntityClick} onExpandClick={onLineageExpand} direction={direction} isCenterNode={tree.data.urn === node.data.urn} /> ); })} ); }