mirror of
https://github.com/datahub-project/datahub.git
synced 2025-07-27 03:19:51 +00:00
149 lines
5.3 KiB
TypeScript
149 lines
5.3 KiB
TypeScript
![]() |
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<NodeData>;
|
||
|
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<EntitySelectParams | undefined>(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<NodeData, 'children'> }[] } = {};
|
||
|
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 (
|
||
|
<Group transform={transformToString(alteredTransform)} top={margin?.top} left={margin?.left}>
|
||
|
{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 (
|
||
|
<LinkHorizontal
|
||
|
data={link}
|
||
|
stroke="black"
|
||
|
strokeWidth="1"
|
||
|
fill="none"
|
||
|
key={`edge-${link.source.data.urn}-${link.target.data.urn}-${direction}`}
|
||
|
data-testid={`edge-${link.source.data.urn}-${link.target.data.urn}-${direction}`}
|
||
|
/>
|
||
|
);
|
||
|
})}
|
||
|
{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 (
|
||
|
<LineageEntityNode
|
||
|
key={`node-${node.data.urn}-${direction}`}
|
||
|
node={node}
|
||
|
isSelected={isSelected}
|
||
|
isHovered={isHovered}
|
||
|
onHover={(select: EntitySelectParams) => setHoveredEntity(select)}
|
||
|
onEntityClick={onEntityClick}
|
||
|
onExpandClick={onLineageExpand}
|
||
|
direction={direction}
|
||
|
isCenterNode={tree.data.urn === node.data.urn}
|
||
|
/>
|
||
|
);
|
||
|
})}
|
||
|
</Group>
|
||
|
);
|
||
|
}
|