import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
import { Card, CardHeader, CardContent } from "../ui/card";
import { Input } from "../ui/input";
import { Dialog, DialogContent } from "../ui/dialog";
import { ScrollArea } from "../ui/scroll-area";
import UnifiedTable from "../IndividualDashboardComponents/UnifiedTable";
import { Home, ZoomIn, ZoomOut } from "lucide-react";
import SliderDemo from "../ui/slider";
const NetworkGraph = ({ data }) => {
// ------------- REFS -------------
const svgRef = useRef(null);
const graphContainerRef = useRef(null);
const zoomRef = useRef(null);
const simulationRef = useRef(null);
// ------------- STATE MANAGEMENT -------------
const [nodeSize, setNodeSize] = useState(70);
const [edgeThickness, setEdgeThickness] = useState(2);
const [maxTransactions, setMaxTransactions] = useState(0);
const [minTransactions, setMinTransactions] = useState(0);
const [minAmount, setMinAmount] = useState(0);
const [selectedNode, setSelectedNode] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
// ------------- CONSTANTS -------------
const colorPalette = {
Person: "#F1C40F",
Entity: "#3498DB",
CommonEntity: "#2ECC71",
SelfTransfer: "#A020F0",
Debit: "#E74C3C",
Credit: "#2ECC71",
};
const MAIN_ENTITY_IDS = ["Statement 1", "Statement 2"];
// ------------- DATA PROCESSING -------------
useEffect(() => {
if (!data || [Link] === 0) return;
const freqMap = {};
[Link]((tx) => {
const entity = [Link];
if (!entity) return;
freqMap[entity] = (freqMap[entity] || 0) + 1;
});
const maxFreq = [Link](freqMap).length
? [Link](...[Link](freqMap))
: 1;
setMaxTransactions(maxFreq);
// Set minTransactions to 1/3 of maxFreq (not 2/3)
setMinTransactions([Link](maxFreq / 4) || 1);
}, [data]);
const calculateNodeRadius = (label, isMainEntity, type) => {
let baseSize = nodeSize;
let sizeMultiplier = 1;
if (isMainEntity) sizeMultiplier = 1.1;
return baseSize * sizeMultiplier;
};
// Function to handle zoom controls
const handleZoomControl = (action) => {
if (![Link] || ![Link]) return;
const svg = [Link]([Link]);
switch (action) {
case "zoomIn":
[Link]().duration(300).call([Link], 1.3);
break;
case "zoomOut":
[Link]().duration(300).call([Link], 0.7);
break;
case "home":
fitGraphToScreen();
break;
default:
break;
}
};
// Function to fit the graph to screen
const fitGraphToScreen = () => {
if (![Link] || ![Link] || ![Link])
return;
const svg = [Link]([Link]);
const g = [Link]("g");
const nodes = [Link](".node");
if ([Link]()) return;
// Get the SVG viewport size
const width = [Link];
const height = [Link] || 800;
// Calculate bounding box
let minX = Infinity,
maxX = -Infinity,
minY = Infinity,
maxY = -Infinity;
[Link](function () {
const node = [Link](this);
const transform = [Link]("transform");
if (transform) {
const match = /translate\(([^,]+),([^)]+)\)/.exec(transform);
if (match) {
const x = parseFloat(match[1]);
const y = parseFloat(match[2]);
minX = [Link](minX, x);
maxX = [Link](maxX, x);
minY = [Link](minY, y);
maxY = [Link](maxY, y);
}
}
});
// Ensure valid bounding box
if (
minX === Infinity ||
maxX === -Infinity ||
minY === Infinity ||
maxY === -Infinity
) {
svg
.transition()
.duration(500)
.call(
[Link],
[Link](width / 2, height / 2).scale(0.7)
);
return;
}
// Add horizontal padding prioritizing width
const horizPadding = [Link](nodeSize * 4, 200);
const vertPadding = [Link](nodeSize * 2, 100);
minX -= horizPadding;
maxX += horizPadding;
minY -= vertPadding;
maxY += vertPadding;
// Calculate width and height of the graph
const graphWidth = maxX - minX;
const graphHeight = maxY - minY;
if (graphWidth <= 0 || graphHeight <= 0) return;
// Prioritize horizontal scaling if graph is wider than it is tall
const isWide = graphWidth > graphHeight * 1.2;
// Calculate scale differently based on aspect ratio
let scale;
if (isWide) {
// For wide graphs, optimize for width
scale = [Link](width / graphWidth, 0.95);
} else {
// For more square or tall graphs, consider both dimensions
scale = [Link](width / graphWidth, height / graphHeight, 0.9);
}
// Calculate center of the graph
const graphCenterX = (minX + maxX) / 2;
const graphCenterY = (minY + maxY) / 2;
// Calculate translation to center the graph in the viewport
const translateX = width / (2 * scale) - graphCenterX;
const translateY = height / (2 * scale) - graphCenterY;
// Apply transformation with smoother transition
svg
.transition()
.duration(600)
.ease([Link])
.call(
[Link],
[Link](translateX, translateY).scale(scale * 0.95)
);
};
useEffect(() => {
if (![Link]) return;
const handleClick = () => {
setTimeout(fitGraphToScreen, 100);
};
const container = [Link];
[Link]("click", handleClick);
return () => {
[Link]("click", handleClick);
};
}, []);
useEffect(() => {
if (![Link]) return;
// Create a resize observer to handle container size changes
const resizeObserver = new ResizeObserver(() => {
setTimeout(fitGraphToScreen, 200);
});
// Start observing the container
[Link]([Link]);
// Cleanup
return () => {
if ([Link]) {
[Link]([Link]);
}
[Link]();
};
}, []);
setTimeout(() => {
fitGraphToScreen();
}, 500);
// NEW - Add this useEffect near the other ones
useEffect(() => {
if (![Link]) return;
// Create a resize observer to handle container size changes
const resizeObserver = new ResizeObserver(() => {
setTimeout(fitGraphToScreen, 200);
});
// Start observing the container
[Link]([Link]);
// Cleanup
return () => {
if ([Link]) {
[Link]([Link]);
}
[Link]();
};
}, []);
useEffect(() => {
if (
!data ||
[Link] === 0 ||
![Link] ||
![Link]
)
return;
[Link]([Link]).selectAll("*").remove();
try {
// ------------- STEP 1: COMPUTE ENTITY FREQUENCIES -------------
const entityFrequency = {};
[Link]((row) => {
const entity = [Link];
if (!entity) return;
entityFrequency[entity] = (entityFrequency[entity] || 0) + 1;
});
// ------------- STEP 2: FILTER TRANSACTIONS -------------
const filteredRows = [Link]((row) => {
const freq = entityFrequency[[Link]] || 0;
const debit = [Link] || 0;
const credit = [Link] || 0;
if (freq < minTransactions) return false;
if (debit < minAmount && credit < minAmount) return false;
return true;
});
if (![Link]) {
const width = [Link];
const height = 600;
const svg = d3
.select([Link])
.attr("width", width)
.attr("height", height);
svg
.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("text-anchor", "middle")
.attr("font-size", "20px")
.text("No data matches the current filter criteria");
return;
}
// ------------- STEP 3: IDENTIFY DISTINCT NODES -------------
const nameSet = new Set();
const entitySet = new Set();
const entityConnections = {};
const selfTransfers = new Map();
[Link]((row) => {
const person = [Link];
const entity = [Link];
const debit = [Link] || 0;
const credit = [Link] || 0;
if (person) {
[Link](person);
}
if (entity) {
if (!entityConnections[entity]) {
entityConnections[entity] = new Set();
}
entityConnections[entity].add(person);
}
if (
person &&
entity &&
person === entity &&
(debit > 0 || credit > 0)
) {
if () {
[Link](person, {
debit: 0,
credit: 0,
total: 0,
debitTransactions: [],
creditTransactions: [],
});
}
const selfTransferData = [Link](person);
if (debit > 0) {
[Link] += debit;
[Link] += debit;
[Link](row);
}
if (credit > 0) {
[Link] += credit;
[Link] += credit;
[Link](row);
}
[Link](person, selfTransferData);
}
});
[Link](entityConnections).forEach((entity) => {
if () {
[Link](entity);
}
});
// ------------- STEP 4: BUILD EDGES -------------
// Separate edges for debit and credit
const edges = [];
[Link]((row) => {
const person = [Link];
const entity = [Link];
const debit = [Link] || 0;
const credit = [Link] || 0;
if (!person || !entity || person === entity) return;
// Handle debit transactions (Person -> Entity)
if (debit > 0) {
const existingEdge = [Link](
(e) =>
[Link] === person && [Link] === entity && [Link] === "debit"
);
if (existingEdge) {
[Link] += debit;
[Link](row);
} else {
[Link]({
source: person,
target: entity,
type: "debit",
value: debit,
transactions: [row],
});
}
}
// Handle credit transactions (Entity -> Person)
if (credit > 0) {
const existingEdge = [Link](
(e) =>
[Link] === entity && [Link] === person && [Link] === "credit"
);
if (existingEdge) {
[Link] += credit;
[Link](row);
} else {
[Link]({
source: entity,
target: person,
type: "credit",
value: credit,
transactions: [row],
});
}
}
});
// ------------- STEP 5: PREPARE GRAPH DATA -------------
const nodes = [];
for (let person of nameSet) {
const isMainEntity = MAIN_ENTITY_IDS.includes(person);
const hasSelfTransfer = [Link](person);
[Link]({
id: person,
label: person,
type: "Person",
isMainEntity,
hasSelfTransfer,
selfTransferData: hasSelfTransfer ? [Link](person) : null,
radius: calculateNodeRadius(person, isMainEntity, "Person"),
color: [Link],
});
}
for (let entity of entitySet) {
const connectedPeople = entityConnections[entity] || new Set();
const isCommon = [Link] >= 2;
const isMainEntity = MAIN_ENTITY_IDS.includes(entity);
const hasSelfTransfer = [Link](entity);
[Link]({
id: entity,
label: entity,
type: isCommon ? "CommonEntity" : "Entity",
isMainEntity,
hasSelfTransfer,
selfTransferData: hasSelfTransfer ? [Link](entity) : null,
radius: calculateNodeRadius(
entity,
isMainEntity,
isCommon ? "CommonEntity" : "Entity"
),
color: isCommon ? [Link] : [Link],
});
}
// ------------- STEP 6: SETUP D3 VISUALIZATION -------------
const width = [Link];
const height = 600;
const svg = d3
.select([Link])
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");
// Define arrow markers
svg
.append("defs")
.selectAll("marker")
.data(["debit", "credit", "selftransfer"])
.enter()
.append("marker")
.attr("id", (d) => `arrow-${d}`)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", (d) => {
if (d === "debit") return [Link];
if (d === "credit") return [Link];
return [Link];
});
const g = [Link]("g");
// Define the arrow marker
const defs = [Link]("defs");
defs
.append("marker")
.attr("id", "arrow-selftransfer")
.attr("viewBox", "0 0 10 10")
.attr("refX", "7") // Adjust refX for better alignment
.attr("refY", "5")
.attr("markerWidth", "6") // Reduced width
.attr("markerHeight", "6")
.attr("orient", "auto-start-reverse") // Ensures it follows path direction
.append("path")
.attr("d", "M 0 0 L 10 5 L 0 10 z") // Triangle shape
.attr("fill", "#6A0DAD");
const zoom = d3
.zoom()
.scaleExtent([0.1, 4])
.on("zoom", (event) => {
[Link]("transform", [Link]);
});
// Store the zoom reference for the control buttons
[Link] = zoom;
[Link](zoom);
// Initial transform to center the graph - will be adjusted later with
fitGraphToScreen
[Link](
[Link],
[Link](width / 2, height / 2).scale(0.8)
);
// Update around line 437 - in the simulation definition
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(edges)
.id((d) => [Link])
.distance((d) => {
const isMainEntity =
MAIN_ENTITY_IDS.includes([Link]) ||
MAIN_ENTITY_IDS.includes([Link]);
if (
MAIN_ENTITY_IDS.includes([Link]) &&
MAIN_ENTITY_IDS.includes([Link])
) {
return 400; // Increased distance between main entities for better
horizontal spread
} else if (isMainEntity) {
return 180; // Connected entities should be at optimal distance
} else {
return 200;
}
})
)
// Add x-positioning force to encourage horizontal layout
.force(
"x",
[Link]().strength((d) => {
// Stronger x-positioning for main entities
return MAIN_ENTITY_IDS.includes([Link]) ? 0.2 : 0.05;
})
)
// Reduce y-positioning force
.force(
"y",
[Link]().strength((d) => {
// Weaker y-positioning to allow horizontal spread
return MAIN_ENTITY_IDS.includes([Link]) ? 0.05 : 0.08;
})
)
.force("charge", [Link]().strength(-1500)) // Stronger repulsion
.force(
"collision",
[Link]().radius((d) => {
return MAIN_ENTITY_IDS.includes([Link])
? [Link] * 2.2 // Slightly increased
: [Link] * 1.8; // Slightly increased
})
)
.force("center", [Link](width / 2, height / 2))
.alphaDecay(0.03) // Slower decay for better settling
.velocityDecay(0.5);
// Store the simulation reference
[Link] = simulation;
const lineGenerator = d3
.line()
.x((d) => d.x)
.y((d) => d.y);
const link = g
.append("g")
.attr("stroke-opacity", 0.6)
.selectAll("path")
.data(edges)
.join("path")
.attr("class", "edge")
.attr("fill", "none")
.attr("stroke", (d) =>
[Link] === "debit" ? [Link] : [Link]
)
.attr("stroke-width", edgeThickness)
.attr("marker-end", (d) => `url(#arrow-${[Link]})`)
.attr("d", (d) =>
lineGenerator([
{ x: [Link].x, y: [Link].y },
{ x: [Link].x, y: [Link].y },
])
);
// FIXED DRAG FUNCTIONALITY
const drag = d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
const node = g
.append("g")
.selectAll("g")
.data(nodes)
.join("g")
.attr("class", "node")
.call(drag) // Apply the drag behavior
.on("click", (event, d) => {
// Prevent the click event from firing during drag
if ([Link]) return;
setSelectedNode({
id: [Link],
data: {
type: [Link],
isMainEntity: [Link],
hasSelfTransfer: [Link],
selfTransferData: [Link],
},
});
setIsDialogOpen(true);
});
node
.append("circle")
.attr("r", (d) => [Link])
.attr("fill", (d) => [Link])
.attr("stroke", "#2C3E50")
.attr("stroke-width", (d) => ([Link] ? 4 : 2));
// FINAL SELF-TRANSFER LOOP AT TOP-CENTER (REFRESH STYLES)
[Link](function (d) {
if ([Link]) {
const selfTransferData = [Link];
const nodeRadius = [Link];
const g = [Link](this);
// Create a unique marker ID for this node to prevent conflicts
const markerId = `arrow-self-${[Link]
.replace(/\s+/g, "-")
.replace(/[^\w-]/g, "")}`;
// Define marker in defs for the arrow head
const defs = [Link]("defs");
if (.node()) {
defs
.append("marker")
.attr("id", markerId)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 8) // Position the arrowhead properly
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", [Link]);
}
// Position parameters - move loop above the node like in reference
const loopRadius = nodeRadius * 0.5; // Make loop larger relative to node
const loopCenterX = 0;
const loopCenterY = -nodeRadius * 0.1; // Position above the node
// Create the self-loop using a path instead of a circle for better
control
// Using arc path for better control over the arrow placement
const arcGenerator = d3
.arc()
.innerRadius(loopRadius)
.outerRadius(loopRadius)
.startAngle(0)
.endAngle(2 * [Link] * 0.97); // Leave small gap for arrow (end at
~350 degrees)
// Create the main loop path
[Link]("path")
.attr("d", arcGenerator)
.attr("transform", `translate(${loopCenterX},${loopCenterY})`)
.attr("fill", "none")
.attr("stroke", [Link])
.attr("stroke-width", 2.5)
.attr("marker-end", `url(#${markerId})`);
}
});
// Node Labels
// In the [Link] function - around line 620
[Link](function (d) {
const nodeEl = [Link](this);
const words = [Link](/\s+/);
const lineHeight = [Link](12, [Link] * 0.25); // Scale lineHeight with
node radius
// Better font size calculation - proportional to node radius
const fontSize = [Link](10, [Link](16, [Link] * 0.3)); // Min 10px,
max 16px, otherwise proportional
const yOffset = [Link] * 0.1;
// Remove any previous labels to prevent duplication
[Link](".node-label").remove();
const text = nodeEl
.append("text")
.attr("class", "node-label")
.attr("text-anchor", "middle")
.attr("dy", `-${(([Link] - 1) * lineHeight) / 2}`)
.attr("fill", "#2C3E50")
.style("font-size", `${fontSize}px`)
.style("font-weight", "bold");
[Link]((word, i) => {
text
.append("tspan")
.attr("x", 0)
.attr("y", yOffset + i * lineHeight)
.text(word);
});
// Also adjust the self-transfer amount font size
if ([Link] && [Link]) {
nodeEl
.append("text")
.attr("class", "main-transfer-amount")
.attr("text-anchor", "middle")
.attr("x", 0)
.attr("y", yOffset + [Link] * lineHeight + lineHeight / 2) //
Scale this offset too
.attr("fill", [Link])
.attr("font-size", `${fontSize * 0.9}px`) // Slightly smaller than
label
.attr("font-weight", "bold")
.text(`₹${[Link]()}`);
}
});
// Create separate groups for debit and credit edge labels to better manage
them
const debitEdgeLabels = g
.append("g")
.selectAll("g")
.data([Link]((d) => [Link] === "debit"))
.join("g");
const creditEdgeLabels = g
.append("g")
.selectAll("g")
.data([Link]((d) => [Link] === "credit"))
.join("g");
// Add text labels for debit edges with improved visibility and positioning
debitEdgeLabels
.append("text")
.attr("text-anchor", "middle")
.attr("dy", "-0.5em") // Position above the line
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", [Link])
.attr("stroke", "white")
.attr("stroke-width", "3px")
.attr("paint-order", "stroke")
.text((d) => `₹${[Link]()}`);
// Add text labels for credit edges with improved visibility and positioning
creditEdgeLabels
.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1.2em") // Position below the line
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", [Link])
.attr("stroke", "white")
.attr("stroke-width", "3px")
.attr("paint-order", "stroke")
.text((d) => `₹${[Link]()}`);
[Link](100);
updatePositions();
[Link]();
// Initialize the graph view after a short delay to ensure positions are
calculated
setTimeout(() => {
fitGraphToScreen();
}, 500);
// Inside your updatePositions() function, replace the debitEdgeLabels and
creditEdgeLabels transform code with this improved version:
function updatePositions() {
[Link]("transform", (d) => `translate(${d.x},${d.y})`);
// Improved edge path creation with greater separation between debit and
credit
[Link]("d", (d) => {
const sourceNode = [Link]((n) => [Link] === [Link]);
const targetNode = [Link]((n) => [Link] === [Link]);
if (!sourceNode || !targetNode) return "";
const sourceRadius = [Link];
const targetRadius = [Link];
const dx = [Link].x - [Link].x;
const dy = [Link].y - [Link].y;
const distance = [Link](dx * dx + dy * dy);
if (distance === 0) return "";
const offsetSourceX = [Link].x + (dx / distance) * sourceRadius;
const offsetSourceY = [Link].y + (dy / distance) * sourceRadius;
const offsetTargetX = [Link].x - (dx / distance) * targetRadius;
const offsetTargetY = [Link].y - (dy / distance) * targetRadius;
const perpX = -dy / distance;
const perpY = dx / distance;
// Increased separation between debit and credit edges
const offsetMultiplier = [Link] === "debit" ? 1 : -1;
const offsetDistance = 25 * offsetMultiplier; // Larger offset for better
separation
const startX = offsetSourceX + perpX * offsetDistance;
const startY = offsetSourceY + perpY * offsetDistance;
const endX = offsetTargetX + perpX * offsetDistance;
const endY = offsetTargetY + perpY * offsetDistance;
// Create a curved path for better visual separation
const midX = (startX + endX) / 2;
const midY = (startY + endY) / 2;
const curvature = 0.2; // Increased curvature for better separation
const controlX = midX + perpX * distance * curvature;
const controlY = midY + perpY * distance * curvature;
// Store the path coordinates for label positioning
[Link] = {
startX,
startY,
endX,
endY,
controlX,
controlY,
midX,
midY,
perpX,
perpY,
distance,
};
return `M${startX},${startY} Q${controlX},${controlY} ${endX},${endY}`;
});
// Position debit edge labels with improved offset to follow the curve
[Link]("transform", (d) => {
if (![Link] || ![Link] || ![Link])
return "translate(0,0)";
const coords = [Link];
// Calculate position along the quadratic curve at t=0.5 (midpoint)
// For a quadratic Bezier curve, the point at parameter t is:
// B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂
// where P₀ is start point, P₁ is control point, P₂ is end point
const t = 0.5;
const t1 = 1 - t;
// Calculate point on the curve
const pointX =
t1 * t1 * [Link] +
2 * t1 * t * [Link] +
t * t * [Link];
const pointY =
t1 * t1 * [Link] +
2 * t1 * t * [Link] +
t * t * [Link];
// Calculate tangent direction to the curve at this point
const tangentX =
2 * (1 - t) * ([Link] - [Link]) +
2 * t * ([Link] - [Link]);
const tangentY =
2 * (1 - t) * ([Link] - [Link]) +
2 * t * ([Link] - [Link]);
// Normalize tangent vector
const tangentLength = [Link](
tangentX * tangentX + tangentY * tangentY
);
const normalizedTangentX = tangentX / tangentLength;
const normalizedTangentY = tangentY / tangentLength;
// Get perpendicular vector (normal)
const normalX = -normalizedTangentY;
const normalY = normalizedTangentX;
// For debit, offset in direction of normal (positive for debit)
const labelOffsetDistance = 15;
const labelX = pointX + normalX * labelOffsetDistance;
const labelY = pointY + normalY * labelOffsetDistance;
return `translate(${labelX},${labelY})`;
});
// Position credit edge labels with improved offset to follow the curve
[Link]("transform", (d) => {
if (![Link] || ![Link] || ![Link])
return "translate(0,0)";
const coords = [Link];
// Calculate position along the quadratic curve at t=0.5 (midpoint)
const t = 0.5;
const t1 = 1 - t;
// Calculate point on the curve
const pointX =
t1 * t1 * [Link] +
2 * t1 * t * [Link] +
t * t * [Link];
const pointY =
t1 * t1 * [Link] +
2 * t1 * t * [Link] +
t * t * [Link];
// Calculate tangent direction to the curve at this point
const tangentX =
2 * (1 - t) * ([Link] - [Link]) +
2 * t * ([Link] - [Link]);
const tangentY =
2 * (1 - t) * ([Link] - [Link]) +
2 * t * ([Link] - [Link]);
// Normalize tangent vector
const tangentLength = [Link](
tangentX * tangentX + tangentY * tangentY
);
const normalizedTangentX = tangentX / tangentLength;
const normalizedTangentY = tangentY / tangentLength;
// Get perpendicular vector (normal)
const normalX = -normalizedTangentY;
const normalY = normalizedTangentX;
// For credit, offset in opposite direction of normal (negative for
credit)
const labelOffsetDistance = -15;
const labelX = pointX + normalX * labelOffsetDistance;
const labelY = pointY + normalY * labelOffsetDistance;
return `translate(${labelX},${labelY})`;
});
}
// ENHANCED DRAG FUNCTIONS
function dragstarted(event, d) {
if (![Link]) [Link](0.3).restart();
[Link] = d.x;
[Link] = d.y;
// Prevent click from firing
[Link]();
}
function dragged(event, d) {
[Link] = event.x;
[Link] = event.y;
// Update positions during drag for smoother experience
updatePositions();
}
// Update dragended function - around line 790
function dragended(event, d) {
if (![Link]) [Link](0);
updatePositions();
// NEW: Ensure graph stays in view after dragging operations
setTimeout(fitGraphToScreen, 100);
}
} catch (error) {
[Link]("Error rendering network graph:", error);
const width = [Link];
const height = 600;
const svg = d3
.select([Link])
.attr("width", width)
.attr("height", height);
svg
.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("text-anchor", "middle")
.attr("font-size", "16px")
.attr("fill", "red")
.text("Error rendering graph. Please check console for details.");
}
}, [data, minTransactions, minAmount, nodeSize, edgeThickness]);
const nodeTransactions = [Link](() => {
if (!selectedNode) return [];
return [Link](
(tx) => [Link] === [Link] || [Link] === [Link]
);
}, [selectedNode, data]);
return (
<div className="space-y-4">
<Card className="w-full max-w-screen-4xl mx-auto shadow-md border border-
gray-300">
<CardContent className="p-6">
<div className="grid grid-cols-3 gap-8 items-start w-full">
{/* Node Size */}
<div className="flex flex-col w-full">
<label className="text-sm font-semibold mb-2">Node Size</label>
<div className="mt-1 rounded-lg w-full flex">
<SliderDemo
defaultValue={[nodeSize]}
max={100}
step={5}
onChange={(value) => setNodeSize(value[0])}
className="w-full"
/>
</div>
</div>
{/* Frequency (Min Transactions) */}
<div className="flex flex-col w-full">
<label className="text-sm font-semibold mb-2">
Frequency (Min Transactions: {minTransactions})
</label>
<div className="mt-1 rounded-lg w-full flex">
{minTransactions !== undefined && minTransactions !== null ? (
// Find and replace the min transactions slider - around line 857
<SliderDemo
value={[minTransactions]}
min={1}
max={maxTransactions || 10}
step={1}
onChange={(value) => setMinTransactions(value[0])}
className="w-full"
/>
) : null}
</div>
</div>
{/* Min Amount (₹) */}
<div className="flex flex-col w-full">
<label className="text-sm font-semibold mb-2">
Min Amount (₹)
</label>
<Input
type="number"
value={minAmount}
onChange={(e) => setMinAmount(Number([Link] || 0))}
placeholder="Enter minimum amount"
className="w-full px-3 py-2 border border-gray-300 rounded-md
shadow-sm mt-1"
/>
</div>
</div>
</CardContent>
</Card>
<div
ref={graphContainerRef}
className="h-[600px] border rounded-lg bg-white relative"
>
{/* Zoom control buttons */}
<div className="absolute top-4 left-4 flex space-x-2 z-10 bg-white bg-
opacity-80 p-1 rounded-md shadow-md">
<button
onClick={() => handleZoomControl("home")}
className="p-2 bg-gray-100 rounded hover:bg-gray-200 transition-colors"
title="Reset view"
>
<Home size={18} />
</button>
<button
onClick={() => handleZoomControl("zoomIn")}
className="p-2 bg-gray-100 rounded hover:bg-gray-200 transition-colors"
title="Zoom in"
>
<ZoomIn size={18} />
</button>
<button
onClick={() => handleZoomControl("zoomOut")}
className="p-2 bg-gray-100 rounded hover:bg-gray-200 transition-colors"
title="Zoom out"
>
<ZoomOut size={18} />
</button>
</div>
<svg ref={svgRef} width="100%" height="600"></svg>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-7xl overflow-auto">
<ScrollArea className="h-[80vh]">
<UnifiedTable
title={`All Transactions for ${selectedNode?.id}`}
data={nodeTransactions || []}
/>
</ScrollArea>
</DialogContent>
</Dialog>
</div>
);
};
export default NetworkGraph;