mirror of
https://github.com/reconurge/flowsint.git
synced 2026-04-30 19:29:26 -05:00
feat: remove unused utils functions
This commit is contained in:
@@ -14,7 +14,7 @@ The project is organized into autonomous modules:
|
||||
- **flowsint-types**: Pydantic models and type definitions
|
||||
- **flowsint-transforms**: Transform modules, scanning logic, and tools
|
||||
- **flowsint-api**: FastAPI server, API routes, and schemas only
|
||||
- **flowsint-app**: Frontend application (unchanged)
|
||||
- **flowsint-app**: Frontend application
|
||||
|
||||
### Module dependencies
|
||||
|
||||
|
||||
@@ -26,20 +26,15 @@ def clean_context(context: List[Dict]) -> List[Dict]:
|
||||
if isinstance(item, dict):
|
||||
# Create a copy and remove unwanted keys
|
||||
cleaned_item = item["data"].copy()
|
||||
|
||||
# Remove top-level keys
|
||||
cleaned_item.pop("id", None)
|
||||
cleaned_item.pop("sketch_id", None)
|
||||
|
||||
# Remove from data if it exists
|
||||
if "data" in cleaned_item and isinstance(cleaned_item["data"], dict):
|
||||
cleaned_item["data"].pop("sketch_id", None)
|
||||
|
||||
# Remove measured/dimensions
|
||||
cleaned_item.pop("measured", None)
|
||||
|
||||
cleaned.append(cleaned_item)
|
||||
print(cleaned)
|
||||
return cleaned
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" theme="dark">
|
||||
<head>
|
||||
<!-- <script
|
||||
<script
|
||||
crossOrigin="anonymous"
|
||||
src="//unpkg.com/react-scan/dist/auto.global.js"
|
||||
>
|
||||
</script> -->
|
||||
</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-check-icon lucide-mail-check"><path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/><path d="m16 19 2 2 4-4"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-at-sign-icon lucide-at-sign"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/></svg>
|
||||
|
Before Width: | Height: | Size: 388 B After Width: | Height: | Size: 318 B |
@@ -22,7 +22,7 @@ import TransformNode from "./transform-node"
|
||||
import TypeNode from "./type-node"
|
||||
import { type TransformNodeData } from "@/types/transform"
|
||||
import { FlowControls } from "./controls"
|
||||
import { getDagreLayoutedElements } from "@/lib/utils"
|
||||
import { getFlowDagreLayoutedElements } from "@/lib/utils"
|
||||
import { toast } from "sonner"
|
||||
import { SaveModal } from "./save-modal"
|
||||
import { useConfirm } from "@/components/use-confirm-dialog"
|
||||
@@ -302,7 +302,7 @@ const FlowEditor = memo(({ initialEdges, initialNodes, theme, flow }: FlowEditor
|
||||
const onLayout = useCallback(() => {
|
||||
// Wait for nodes to be measured before running layout
|
||||
setTimeout(() => {
|
||||
const layouted = getDagreLayoutedElements(
|
||||
const layouted = getFlowDagreLayoutedElements(
|
||||
nodes.map(node => ({
|
||||
...node,
|
||||
measured: {
|
||||
|
||||
@@ -151,17 +151,14 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
allowLasso = false,
|
||||
minimap = false
|
||||
}) => {
|
||||
const [currentZoom, setCurrentZoom] = useState({
|
||||
k: 1,
|
||||
x: 1,
|
||||
y: 1
|
||||
});
|
||||
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
||||
const isLassoActive = useGraphControls(s => s.isLassoActive)
|
||||
// Hover highlighting state
|
||||
const [highlightNodes, setHighlightNodes] = useState<Set<string>>(new Set());
|
||||
const [highlightLinks, setHighlightLinks] = useState<Set<string>>(new Set());
|
||||
const [hoverNode, setHoverNode] = useState<string | null>(null);
|
||||
const zoomRef = useRef({ k: 1, x: 0, y: 0 });
|
||||
const [zoomState, setZoomState] = useState({ k: 1, x: 0, y: 0 });
|
||||
|
||||
// Store references
|
||||
const graphRef = useRef<any>();
|
||||
@@ -211,6 +208,11 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
}
|
||||
}, [nodes, showIcons]);
|
||||
|
||||
const handleZoom = useCallback((zoom: any) => {
|
||||
zoomRef.current = zoom;
|
||||
setZoomState(zoom);
|
||||
}, []);
|
||||
|
||||
// Optimized graph initialization callback
|
||||
const initializeGraph = useCallback((graphInstance: any) => {
|
||||
if (!graphInstance) return;
|
||||
@@ -315,8 +317,8 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
|
||||
// Memoized rendering check
|
||||
const shouldUseSimpleRendering = useMemo(() =>
|
||||
nodes.length > CONSTANTS.NODE_COUNT_THRESHOLD || currentZoom.k < 1.5
|
||||
, [nodes.length, currentZoom]);
|
||||
nodes.length > CONSTANTS.NODE_COUNT_THRESHOLD || zoomState.k < 1.5
|
||||
, [nodes.length, zoomState.k]);
|
||||
|
||||
// Memoized graph data transformation with proper memoization dependencies
|
||||
const graphData = useMemo(() => {
|
||||
@@ -350,12 +352,11 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
const group = edgeGroups.get(key)!;
|
||||
const groupIndex = group.indexOf(edge);
|
||||
const groupSize = group.length;
|
||||
const curve = groupSize > 1 ? (groupIndex - (groupSize - 1) / 2) * 0.2 : 0;
|
||||
|
||||
const curvature = groupSize > 1 ? (groupIndex - (groupSize - 1) / 2) * 0.2 : 0;
|
||||
return {
|
||||
...edge,
|
||||
edgeLabel: edge.label,
|
||||
curve,
|
||||
curvature,
|
||||
groupIndex,
|
||||
groupSize
|
||||
};
|
||||
@@ -421,7 +422,7 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
if (!showLabels) return new Set<string>();
|
||||
|
||||
// Find the appropriate layer for current zoom
|
||||
const currentLayer = CONSTANTS.LABEL_LAYERS.find(layer => currentZoom.k >= layer.minZoom);
|
||||
const currentLayer = CONSTANTS.LABEL_LAYERS.find(layer => zoomState.k >= layer.minZoom);
|
||||
if (!currentLayer) return new Set<string>();
|
||||
|
||||
// Sort nodes by weight (number of connections) in descending order
|
||||
@@ -440,7 +441,7 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
.slice(0, currentLayer.maxNodes);
|
||||
|
||||
return new Set(visibleNodes.map(node => node.id));
|
||||
}, [graphData.nodes, currentZoom, showLabels]);
|
||||
}, [graphData.nodes, zoomState.k, showLabels]);
|
||||
|
||||
// Event handlers with proper memoization
|
||||
const handleNodeClick = useCallback((node: any, event: MouseEvent) => {
|
||||
@@ -519,7 +520,6 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
if (node) {
|
||||
const weight = node.neighbors?.length || 0;
|
||||
const label = node.nodeLabel || node.label || node.id;
|
||||
|
||||
// Position tooltip above the node using the graph's coordinate conversion
|
||||
if (graphRef.current) {
|
||||
try {
|
||||
@@ -582,12 +582,10 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
if (link) {
|
||||
// Add the hovered link
|
||||
newHighlightLinks.add(`${link.source}-${link.target}`);
|
||||
|
||||
// Add connected nodes
|
||||
newHighlightNodes.add(link.source.id);
|
||||
newHighlightNodes.add(link.target.id);
|
||||
}
|
||||
|
||||
setHoverNode(null);
|
||||
setHighlightNodes(newHighlightNodes);
|
||||
setHighlightLinks(newHighlightLinks);
|
||||
@@ -600,7 +598,6 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
const isHighlighted = highlightNodes.has(node.id) || isSelected(node.id);
|
||||
const hasAnyHighlight = highlightNodes.size > 0 || highlightLinks.size > 0;
|
||||
const isHovered = hoverNode === node.id || (isCurrent(node.id));
|
||||
|
||||
// Draw highlight ring for highlighted nodes
|
||||
if (isHighlighted) {
|
||||
ctx.beginPath();
|
||||
@@ -608,21 +605,17 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
ctx.fillStyle = isHovered ? GRAPH_COLORS.NODE_HIGHLIGHT_HOVER : GRAPH_COLORS.NODE_HIGHLIGHT_DEFAULT;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Set node color based on highlight state
|
||||
if (hasAnyHighlight) {
|
||||
ctx.fillStyle = isHighlighted ? node.nodeColor : `${node.nodeColor}7D`;
|
||||
} else {
|
||||
ctx.fillStyle = node.nodeColor;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, size, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
// Early exit for simple rendering
|
||||
if (shouldUseSimpleRendering) return;
|
||||
|
||||
// Optimized icon rendering with cached images
|
||||
if (showIcons && node.nodeType) {
|
||||
const cachedImage = imageCache.get(node.nodeType);
|
||||
@@ -634,7 +627,6 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optimized label rendering with layered display
|
||||
if (showLabels && globalScale > 3) {
|
||||
const label = truncateText(node.nodeLabel || node.label || node.id, 58);
|
||||
@@ -672,7 +664,6 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
let strokeStyle: string;
|
||||
let lineWidth: number;
|
||||
let fillStyle: string;
|
||||
|
||||
if (isHighlighted) {
|
||||
strokeStyle = GRAPH_COLORS.LINK_HIGHLIGHTED;
|
||||
fillStyle = GRAPH_COLORS.LINK_HIGHLIGHTED;
|
||||
@@ -686,10 +677,25 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
fillStyle = GRAPH_COLORS.LINK_DEFAULT;
|
||||
lineWidth = CONSTANTS.LINK_WIDTH * (forceSettings.linkWidth.value / 5);
|
||||
}
|
||||
// Draw connection line
|
||||
// Draw connection line (use quadratic curve if curvature present)
|
||||
const curvature: number = link.curvature || 0;
|
||||
const dx = end.x - start.x;
|
||||
const dy = end.y - start.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
const midX = (start.x + end.x) * 0.5;
|
||||
const midY = (start.y + end.y) * 0.5;
|
||||
const normX = -dy / distance;
|
||||
const normY = dx / distance;
|
||||
const offset = curvature * distance;
|
||||
const ctrlX = midX + normX * offset;
|
||||
const ctrlY = midY + normY * offset;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(start.x, start.y);
|
||||
ctx.lineTo(end.x, end.y);
|
||||
if (curvature !== 0) {
|
||||
ctx.quadraticCurveTo(ctrlX, ctrlY, end.x, end.y);
|
||||
} else {
|
||||
ctx.lineTo(end.x, end.y);
|
||||
}
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.stroke();
|
||||
@@ -697,27 +703,38 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
const arrowLength = forceSettings.linkDirectionalArrowLength.value;
|
||||
if (arrowLength && arrowLength > 0) {
|
||||
const arrowRelPos = forceSettings.linkDirectionalArrowRelPos.value || 1;
|
||||
// Calculate arrow position along the link
|
||||
let arrowX = start.x + (end.x - start.x) * arrowRelPos;
|
||||
let arrowY = start.y + (end.y - start.y) * arrowRelPos;
|
||||
// If arrow is at the target node (arrowRelPos = 1), offset it to be at the node's edge
|
||||
if (arrowRelPos === 1) {
|
||||
const dx = end.x - start.x;
|
||||
const dy = end.y - start.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance > 0) {
|
||||
// Calculate target node size (same as in renderNode function)
|
||||
const targetNodeSize = (end.nodeSize || CONSTANTS.NODE_DEFAULT_SIZE) * (forceSettings.nodeSize.value / 100 + 0.4);
|
||||
// Calculate offset to place arrow at node edge
|
||||
const offset = targetNodeSize / distance;
|
||||
arrowX = end.x - dx * offset;
|
||||
arrowY = end.y - dy * offset;
|
||||
// Helper to get point and tangent along straight/curved link
|
||||
const bezierPoint = (t: number) => {
|
||||
if (curvature === 0) {
|
||||
return { x: start.x + dx * t, y: start.y + dy * t };
|
||||
}
|
||||
const oneMinusT = 1 - t;
|
||||
return {
|
||||
x: oneMinusT * oneMinusT * start.x + 2 * oneMinusT * t * ctrlX + t * t * end.x,
|
||||
y: oneMinusT * oneMinusT * start.y + 2 * oneMinusT * t * ctrlY + t * t * end.y,
|
||||
};
|
||||
};
|
||||
const bezierTangent = (t: number) => {
|
||||
if (curvature === 0) {
|
||||
return { x: dx, y: dy };
|
||||
}
|
||||
const oneMinusT = 1 - t;
|
||||
return {
|
||||
x: 2 * oneMinusT * (ctrlX - start.x) + 2 * t * (end.x - ctrlX),
|
||||
y: 2 * oneMinusT * (ctrlY - start.y) + 2 * t * (end.y - ctrlY),
|
||||
};
|
||||
};
|
||||
let t = arrowRelPos;
|
||||
let { x: arrowX, y: arrowY } = bezierPoint(t);
|
||||
if (arrowRelPos === 1) {
|
||||
const tan = bezierTangent(0.99);
|
||||
const tanLen = Math.hypot(tan.x, tan.y) || 1;
|
||||
const targetNodeSize = (end.nodeSize || CONSTANTS.NODE_DEFAULT_SIZE) * (forceSettings.nodeSize.value / 100 + 0.4);
|
||||
arrowX = end.x - (tan.x / tanLen) * targetNodeSize;
|
||||
arrowY = end.y - (tan.y / tanLen) * targetNodeSize;
|
||||
}
|
||||
// Calculate arrow direction
|
||||
const dx = end.x - start.x;
|
||||
const dy = end.y - start.y;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const tan = bezierTangent(t);
|
||||
const angle = Math.atan2(tan.y, tan.x);
|
||||
// Draw arrow head
|
||||
ctx.save();
|
||||
ctx.translate(arrowX, arrowY);
|
||||
@@ -735,12 +752,24 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
if (shouldUseSimpleRendering || !link.label) return;
|
||||
// Only show labels for highlighted links when there's any highlighting
|
||||
if (isHighlighted) {
|
||||
// Calculate label position and angle
|
||||
tempPos.x = (start.x + end.x) * 0.5;
|
||||
tempPos.y = (start.y + end.y) * 0.5;
|
||||
const dx = end.x - start.x;
|
||||
const dy = end.y - start.y;
|
||||
let textAngle = Math.atan2(dy, dx);
|
||||
// Calculate label position and angle along straight/curved link
|
||||
let textAngle: number;
|
||||
if ((link.curvature || 0) !== 0) {
|
||||
// Bezier midpoint and tangent at t=0.5
|
||||
const t = 0.5;
|
||||
const oneMinusT = 1 - t;
|
||||
tempPos.x = oneMinusT * oneMinusT * start.x + 2 * oneMinusT * t * ctrlX + t * t * end.x;
|
||||
tempPos.y = oneMinusT * oneMinusT * start.y + 2 * oneMinusT * t * ctrlY + t * t * end.y;
|
||||
const tx = 2 * oneMinusT * (ctrlX - start.x) + 2 * t * (end.x - ctrlX);
|
||||
const ty = 2 * oneMinusT * (ctrlY - start.y) + 2 * t * (end.y - ctrlY);
|
||||
textAngle = Math.atan2(ty, tx);
|
||||
} else {
|
||||
tempPos.x = (start.x + end.x) * 0.5;
|
||||
tempPos.y = (start.y + end.y) * 0.5;
|
||||
const sdx = end.x - start.x;
|
||||
const sdy = end.y - start.y;
|
||||
textAngle = Math.atan2(sdy, sdx);
|
||||
}
|
||||
// Flip text for readability
|
||||
if (textAngle > CONSTANTS.HALF_PI || textAngle < -CONSTANTS.HALF_PI) {
|
||||
textAngle += textAngle > 0 ? -CONSTANTS.PI : CONSTANTS.PI;
|
||||
@@ -772,7 +801,6 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
// Restart simulation when settings change (debounced)
|
||||
useEffect(() => {
|
||||
let settingsTimeout: number | undefined;
|
||||
|
||||
const restartSimulation = () => {
|
||||
if (graphRef.current && isGraphReadyRef.current) {
|
||||
if (settingsTimeout) clearTimeout(settingsTimeout);
|
||||
@@ -781,10 +809,8 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
}, 100) as any; // Debounce settings changes
|
||||
}
|
||||
};
|
||||
|
||||
// Restart simulation when force settings change
|
||||
restartSimulation();
|
||||
|
||||
return () => {
|
||||
if (settingsTimeout) clearTimeout(settingsTimeout);
|
||||
};
|
||||
@@ -892,7 +918,7 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
onNodeRightClick={handleNodeRightClick}
|
||||
onNodeClick={handleNodeClick}
|
||||
onBackgroundClick={handleBackgroundClick}
|
||||
linkCurvature={link => link.curve}
|
||||
linkCurvature={link => link.curvature || 0}
|
||||
nodeCanvasObject={renderNode}
|
||||
onNodeDragEnd={(node => {
|
||||
node.fx = node.x;
|
||||
@@ -906,8 +932,8 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
warmupTicks={forceSettings.warmupTicks.value}
|
||||
dagLevelDistance={forceSettings.dagLevelDistance.value}
|
||||
backgroundColor={backgroundColor}
|
||||
onZoom={(zoom) => setCurrentZoom(zoom)}
|
||||
onZoomEnd={(zoom) => setCurrentZoom(zoom)}
|
||||
onZoom={handleZoom}
|
||||
onZoomEnd={handleZoom}
|
||||
linkCanvasObject={renderLink}
|
||||
enableNodeDrag={!shouldUseSimpleRendering}
|
||||
autoPauseRedraw={true}
|
||||
@@ -917,7 +943,7 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
|
||||
{allowLasso && isLassoActive && <Lasso nodes={graphData.nodes} graph2ScreenCoords={graph2ScreenCoords} partial={true} width={containerSize.width}
|
||||
height={containerSize.height} />}
|
||||
{minimap && graphData.nodes &&
|
||||
<MiniMap zoomTransform={currentZoom}
|
||||
<MiniMap zoomTransform={zoomState}
|
||||
canvasWidth={containerSize.width}
|
||||
canvasHeight={containerSize.height}
|
||||
nodes={graphData.nodes as GraphNode[]} />}
|
||||
|
||||
@@ -73,10 +73,7 @@ const InfoDialog = () => {
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>
|
||||
<strong>Small to medium datasets (1-500 nodes):</strong> Interactive <strong>React Flow</strong> (<a href="https://reactflow.dev/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">reactflow.dev</a>) graphs with real-time node dragging, detailed tooltips, and smooth animations for precise exploration.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Medium to large datasets (1-1500 nodes):</strong> Optimized <strong>React Force Graph</strong> (<a href="https://github.com/vasturiano/react-force-graph" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">github.com/vasturiano/react-force-graph</a>) layouts with force-directed algorithms that group related entities while maintaining interactive features for focused analysis.
|
||||
<strong>Small to large datasets (1-1500 nodes):</strong> Optimized <strong>React Force Graph</strong> (<a href="https://github.com/vasturiano/react-force-graph" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">github.com/vasturiano/react-force-graph</a>) layouts with force-directed algorithms that group related entities while maintaining interactive features for focused analysis.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Large datasets (1550-100,000 nodes):</strong> High-performance <strong>Cosmograph</strong> (<a href="https://cosmograph.app" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">cosmograph.app</a>) with advanced rendering techniques, allowing you to visualize complex networks without sacrificing responsiveness or browser stability.
|
||||
|
||||
@@ -1,122 +1,13 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import Dagre from '@dagrejs/dagre';
|
||||
|
||||
import { type Edge, Position, type Node } from '@xyflow/react';
|
||||
import * as d3 from "d3-force"
|
||||
import { GraphEdge, GraphNode } from '@/types';
|
||||
|
||||
interface NodePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface NodeMeasured {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface NodeInternals {
|
||||
positionAbsolute: NodePosition;
|
||||
}
|
||||
|
||||
interface FlowNode {
|
||||
measured: NodeMeasured;
|
||||
internals: NodeInternals;
|
||||
}
|
||||
|
||||
interface IntersectionPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
import { FlowEdge, FlowNode } from "@/stores/flow-store";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export const zoomSelector = (s: { transform: number[]; }) => s.transform[2] >= 0.6;
|
||||
|
||||
|
||||
// this helper function returns the intersection point
|
||||
// of the line between the center of the intersectionNode and the target node
|
||||
function getNodeIntersection(
|
||||
intersectionNode: FlowNode,
|
||||
targetNode: FlowNode
|
||||
): IntersectionPoint {
|
||||
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured;
|
||||
const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
|
||||
const targetPosition = targetNode.internals.positionAbsolute;
|
||||
|
||||
const w = intersectionNodeWidth / 2;
|
||||
const h = intersectionNodeHeight / 2;
|
||||
|
||||
const x2 = intersectionNodePosition.x + w;
|
||||
const y2 = intersectionNodePosition.y + h;
|
||||
const x1 = targetPosition.x + targetNode.measured.width / 2;
|
||||
const y1 = targetPosition.y + targetNode.measured.height / 2;
|
||||
|
||||
const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
|
||||
const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
|
||||
const a = 1 / (Math.abs(xx1) + Math.abs(yy1));
|
||||
const xx3 = a * xx1;
|
||||
const yy3 = a * yy1;
|
||||
const x = w * (xx3 + yy3) + x2;
|
||||
const y = h * (-xx3 + yy3) + y2;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
||||
function getEdgePosition(node: FlowNode, intersectionPoint: IntersectionPoint): Position {
|
||||
const n = { ...node.internals.positionAbsolute, ...node };
|
||||
const nx = Math.round(n.x);
|
||||
const ny = Math.round(n.y);
|
||||
const px = Math.round(intersectionPoint.x);
|
||||
const py = Math.round(intersectionPoint.y);
|
||||
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + n.measured.width - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y + n.measured.height - 1) {
|
||||
return Position.Bottom;
|
||||
}
|
||||
|
||||
return Position.Top;
|
||||
}
|
||||
|
||||
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
||||
interface EdgeParams {
|
||||
sx: number;
|
||||
sy: number;
|
||||
tx: number;
|
||||
ty: number;
|
||||
sourcePos: Position;
|
||||
targetPos: Position;
|
||||
}
|
||||
|
||||
export function getEdgeParams(source: FlowNode, target: FlowNode): EdgeParams {
|
||||
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
||||
const targetIntersectionPoint = getNodeIntersection(target, source);
|
||||
|
||||
const sourcePos = getEdgePosition(source, sourceIntersectionPoint);
|
||||
const targetPos = getEdgePosition(target, targetIntersectionPoint);
|
||||
|
||||
return {
|
||||
sx: sourceIntersectionPoint.x,
|
||||
sy: sourceIntersectionPoint.y,
|
||||
tx: targetIntersectionPoint.x,
|
||||
ty: targetIntersectionPoint.y,
|
||||
sourcePos,
|
||||
targetPos,
|
||||
};
|
||||
}
|
||||
|
||||
interface LayoutOptions {
|
||||
direction?: "LR" | "TB";
|
||||
strength?: number;
|
||||
@@ -124,81 +15,9 @@ interface LayoutOptions {
|
||||
iterations?: number;
|
||||
}
|
||||
|
||||
export const getForceLayoutedElements = (
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
options: LayoutOptions = {
|
||||
direction: "LR",
|
||||
strength: -30,
|
||||
distance: 10,
|
||||
iterations: 300,
|
||||
},
|
||||
) => {
|
||||
// Create a map of node IDs to indices for the simulation
|
||||
const nodeMap = new Map(nodes.map((node, i) => [node.id, i]))
|
||||
|
||||
// Create a copy of nodes with positions for the simulation
|
||||
const nodesCopy = nodes.map((node) => ({
|
||||
...node,
|
||||
x: node.position?.x || Math.random() * 500,
|
||||
y: node.position?.y || Math.random() * 500,
|
||||
width: node.measured?.width || 0,
|
||||
height: node.measured?.height || 0,
|
||||
}))
|
||||
|
||||
// Create links for the simulation using indices
|
||||
const links = edges.map((edge) => ({
|
||||
source: nodeMap.get(edge.source),
|
||||
target: nodeMap.get(edge.target),
|
||||
original: edge,
|
||||
}))
|
||||
|
||||
// Create the simulation
|
||||
const simulation = d3
|
||||
.forceSimulation(nodesCopy)
|
||||
.force(
|
||||
"link",
|
||||
d3.forceLink(links).id((d: any) => nodeMap.get(d.id)),
|
||||
)
|
||||
.force("charge", d3.forceManyBody().strength(options.strength || -300))
|
||||
.force("center", d3.forceCenter(250, 250))
|
||||
.force(
|
||||
"collision",
|
||||
d3.forceCollide().radius((d: any) => Math.max(d.width, d.height) / 2 + 10),
|
||||
)
|
||||
|
||||
// If direction is horizontal, adjust forces
|
||||
if (options.direction === "LR") {
|
||||
simulation.force("x", d3.forceX(250).strength(0.1))
|
||||
simulation.force("y", d3.forceY(250).strength(0.05))
|
||||
} else {
|
||||
simulation.force("x", d3.forceX(250).strength(0.05))
|
||||
simulation.force("y", d3.forceY(250).strength(0.1))
|
||||
}
|
||||
|
||||
// Run the simulation synchronously
|
||||
simulation.stop()
|
||||
for (let i = 0; i < (options.iterations || 300); i++) {
|
||||
simulation.tick()
|
||||
}
|
||||
|
||||
// Update node positions based on simulation results
|
||||
const updatedNodes = nodesCopy.map((node) => ({
|
||||
...node,
|
||||
position: {
|
||||
x: node.x - node.width / 2,
|
||||
y: node.y - node.height / 2,
|
||||
},
|
||||
}))
|
||||
|
||||
return {
|
||||
nodes: updatedNodes,
|
||||
edges,
|
||||
}
|
||||
}
|
||||
|
||||
export const getDagreLayoutedElements = (nodes: GraphNode[] | any,
|
||||
edges: GraphEdge[] | any,
|
||||
// dagre layout function for the main graph component.
|
||||
export const getDagreLayoutedElements = (nodes: GraphNode[],
|
||||
edges: GraphEdge[],
|
||||
options: LayoutOptions = {
|
||||
direction: "TB",
|
||||
strength: -300,
|
||||
@@ -228,6 +47,38 @@ export const getDagreLayoutedElements = (nodes: GraphNode[] | any,
|
||||
};
|
||||
};
|
||||
|
||||
// dagre layout function for the flow component.
|
||||
export const getFlowDagreLayoutedElements = (nodes: FlowNode[],
|
||||
edges: FlowEdge[],
|
||||
options: LayoutOptions = {
|
||||
direction: "TB",
|
||||
strength: -300,
|
||||
distance: 10,
|
||||
iterations: 300,
|
||||
},) => {
|
||||
|
||||
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
g.setGraph({ rankdir: options.direction });
|
||||
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
|
||||
nodes.forEach((node) =>
|
||||
g.setNode(node.id, {
|
||||
...node,
|
||||
width: node.measured?.width ?? 0,
|
||||
height: node.measured?.height ?? 0,
|
||||
}),
|
||||
);
|
||||
Dagre.layout(g);
|
||||
return {
|
||||
nodes: nodes.map((node) => {
|
||||
const position = g.node(node.id);
|
||||
const x = position.x - (node.measured?.width ?? 0) / 2;
|
||||
const y = position.y - (node.measured?.height ?? 0) / 2;
|
||||
return { ...node, position: { x, y } };
|
||||
}),
|
||||
edges,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const sanitize = (name: string) => {
|
||||
return name
|
||||
@@ -364,7 +215,7 @@ export function deepObjectDiff(obj1: Dictionary, obj2: Dictionary): Dictionary {
|
||||
diffObject = { ...diffObject, [key]: { value, new: false, oldValue: obj1[key] ?? null, newValue: obj2[key] ?? null, identical: obj2[key] === obj1[key] } }
|
||||
}
|
||||
})
|
||||
// We map over the obj1 key:value duos to retrieve new keys that might have disapeared
|
||||
// We map over the obj1 key:value duos to retrieve keys that might have disapeared
|
||||
Object.entries(obj1).forEach(([key, value]) => {
|
||||
// We check for additional keys
|
||||
if (!obj2.hasOwnProperty(key))
|
||||
|
||||
Reference in New Issue
Block a user