0% found this document useful (0 votes)
6 views

Network Graph Component

The document outlines a React component named NetworkGraph that visualizes transaction data using D3.js. It includes features for zooming, filtering, and dynamically adjusting the graph based on user interactions and data changes. The component processes transaction data to create nodes and edges representing entities and their relationships, while also managing various states and effects for rendering and user interface interactions.

Uploaded by

work.harsh268
Copyright
© © All Rights Reserved
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views

Network Graph Component

The document outlines a React component named NetworkGraph that visualizes transaction data using D3.js. It includes features for zooming, filtering, and dynamically adjusting the graph based on user interactions and data changes. The component processes transaction data to create nodes and edges representing entities and their relationships, while also managing various states and effects for rendering and user interface interactions.

Uploaded by

work.harsh268
Copyright
© © All Rights Reserved
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 20

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 || data.length === 0) return;

const freqMap = {};


data.forEach((tx) => {
const entity = tx.entity;
if (!entity) return;
freqMap[entity] = (freqMap[entity] || 0) + 1;
});

const maxFreq = Object.values(freqMap).length


? Math.max(...Object.values(freqMap))
: 1;

setMaxTransactions(maxFreq);
// Set minTransactions to 1/3 of maxFreq (not 2/3)
setMinTransactions(Math.ceil(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 (!zoomRef.current || !svgRef.current) return;

const svg = d3.select(svgRef.current);

switch (action) {
case "zoomIn":
svg.transition().duration(300).call(zoomRef.current.scaleBy, 1.3);
break;
case "zoomOut":
svg.transition().duration(300).call(zoomRef.current.scaleBy, 0.7);
break;
case "home":
fitGraphToScreen();
break;
default:
break;
}
};

// Function to fit the graph to screen


const fitGraphToScreen = () => {
if (!zoomRef.current || !svgRef.current || !graphContainerRef.current)
return;

const svg = d3.select(svgRef.current);


const g = svg.select("g");
const nodes = d3.selectAll(".node");

if (nodes.empty()) return;

// Get the SVG viewport size


const width = graphContainerRef.current.clientWidth;
const height = graphContainerRef.current.clientHeight || 800;

// Calculate bounding box


let minX = Infinity,
maxX = -Infinity,
minY = Infinity,
maxY = -Infinity;

nodes.each(function () {
const node = d3.select(this);
const transform = node.attr("transform");

if (transform) {
const match = /translate\(([^,]+),([^)]+)\)/.exec(transform);
if (match) {
const x = parseFloat(match[1]);
const y = parseFloat(match[2]);

minX = Math.min(minX, x);


maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
}
}
});

// Ensure valid bounding box


if (
minX === Infinity ||
maxX === -Infinity ||
minY === Infinity ||
maxY === -Infinity
) {
svg
.transition()
.duration(500)
.call(
zoomRef.current.transform,
d3.zoomIdentity.translate(width / 2, height / 2).scale(0.7)
);
return;
}

// Add horizontal padding prioritizing width


const horizPadding = Math.max(nodeSize * 4, 200);
const vertPadding = Math.max(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 = Math.min(width / graphWidth, 0.95);
} else {
// For more square or tall graphs, consider both dimensions
scale = Math.min(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(d3.easeCubicOut)
.call(
zoomRef.current.transform,
d3.zoomIdentity.translate(translateX, translateY).scale(scale * 0.95)
);
};

useEffect(() => {
if (!graphContainerRef.current) return;

const handleClick = () => {


setTimeout(fitGraphToScreen, 100);
};

const container = graphContainerRef.current;


container.addEventListener("click", handleClick);

return () => {
container.removeEventListener("click", handleClick);
};
}, []);

useEffect(() => {
if (!graphContainerRef.current) return;

// Create a resize observer to handle container size changes


const resizeObserver = new ResizeObserver(() => {
setTimeout(fitGraphToScreen, 200);
});

// Start observing the container


resizeObserver.observe(graphContainerRef.current);

// Cleanup
return () => {
if (graphContainerRef.current) {
resizeObserver.unobserve(graphContainerRef.current);
}
resizeObserver.disconnect();
};
}, []);
setTimeout(() => {
fitGraphToScreen();
}, 500);

// NEW - Add this useEffect near the other ones


useEffect(() => {
if (!graphContainerRef.current) return;

// Create a resize observer to handle container size changes


const resizeObserver = new ResizeObserver(() => {
setTimeout(fitGraphToScreen, 200);
});
// Start observing the container
resizeObserver.observe(graphContainerRef.current);

// Cleanup
return () => {
if (graphContainerRef.current) {
resizeObserver.unobserve(graphContainerRef.current);
}
resizeObserver.disconnect();
};
}, []);
useEffect(() => {
if (
!data ||
data.length === 0 ||
!svgRef.current ||
!graphContainerRef.current
)
return;

d3.select(svgRef.current).selectAll("*").remove();

try {
// ------------- STEP 1: COMPUTE ENTITY FREQUENCIES -------------
const entityFrequency = {};
data.forEach((row) => {
const entity = row.entity;
if (!entity) return;
entityFrequency[entity] = (entityFrequency[entity] || 0) + 1;
});

// ------------- STEP 2: FILTER TRANSACTIONS -------------


const filteredRows = data.filter((row) => {
const freq = entityFrequency[row.entity] || 0;
const debit = row.debit || 0;
const credit = row.credit || 0;

if (freq < minTransactions) return false;


if (debit < minAmount && credit < minAmount) return false;

return true;
});

if (!filteredRows.length) {
const width = graphContainerRef.current.clientWidth;
const height = 600;

const svg = d3
.select(svgRef.current)
.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();

filteredRows.forEach((row) => {
const person = row.name;
const entity = row.entity;
const debit = row.debit || 0;
const credit = row.credit || 0;

if (person) {
nameSet.add(person);
}

if (entity) {
if (!entityConnections[entity]) {
entityConnections[entity] = new Set();
}
entityConnections[entity].add(person);
}

if (
person &&
entity &&
person === entity &&
(debit > 0 || credit > 0)
) {
if (!selfTransfers.has(person)) {
selfTransfers.set(person, {
debit: 0,
credit: 0,
total: 0,
debitTransactions: [],
creditTransactions: [],
});
}

const selfTransferData = selfTransfers.get(person);

if (debit > 0) {
selfTransferData.debit += debit;
selfTransferData.total += debit;
selfTransferData.debitTransactions.push(row);
}

if (credit > 0) {
selfTransferData.credit += credit;
selfTransferData.total += credit;
selfTransferData.creditTransactions.push(row);
}

selfTransfers.set(person, selfTransferData);
}
});

Object.keys(entityConnections).forEach((entity) => {
if (!nameSet.has(entity)) {
entitySet.add(entity);
}
});

// ------------- STEP 4: BUILD EDGES -------------


// Separate edges for debit and credit
const edges = [];

filteredRows.forEach((row) => {
const person = row.name;
const entity = row.entity;
const debit = row.debit || 0;
const credit = row.credit || 0;

if (!person || !entity || person === entity) return;

// Handle debit transactions (Person -> Entity)


if (debit > 0) {
const existingEdge = edges.find(
(e) =>
e.source === person && e.target === entity && e.type === "debit"
);

if (existingEdge) {
existingEdge.value += debit;
existingEdge.transactions.push(row);
} else {
edges.push({
source: person,
target: entity,
type: "debit",
value: debit,
transactions: [row],
});
}
}

// Handle credit transactions (Entity -> Person)


if (credit > 0) {
const existingEdge = edges.find(
(e) =>
e.source === entity && e.target === person && e.type === "credit"
);

if (existingEdge) {
existingEdge.value += credit;
existingEdge.transactions.push(row);
} else {
edges.push({
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 = selfTransfers.has(person);

nodes.push({
id: person,
label: person,
type: "Person",
isMainEntity,
hasSelfTransfer,
selfTransferData: hasSelfTransfer ? selfTransfers.get(person) : null,
radius: calculateNodeRadius(person, isMainEntity, "Person"),
color: colorPalette.Person,
});
}

for (let entity of entitySet) {


const connectedPeople = entityConnections[entity] || new Set();
const isCommon = connectedPeople.size >= 2;
const isMainEntity = MAIN_ENTITY_IDS.includes(entity);
const hasSelfTransfer = selfTransfers.has(entity);

nodes.push({
id: entity,
label: entity,
type: isCommon ? "CommonEntity" : "Entity",
isMainEntity,
hasSelfTransfer,
selfTransferData: hasSelfTransfer ? selfTransfers.get(entity) : null,
radius: calculateNodeRadius(
entity,
isMainEntity,
isCommon ? "CommonEntity" : "Entity"
),
color: isCommon ? colorPalette.CommonEntity : colorPalette.Entity,
});
}

// ------------- STEP 6: SETUP D3 VISUALIZATION -------------


const width = graphContainerRef.current.clientWidth;
const height = 600;

const svg = d3
.select(svgRef.current)
.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 colorPalette.Debit;
if (d === "credit") return colorPalette.Credit;
return colorPalette.SelfTransfer;
});

const g = svg.append("g");
// Define the arrow marker
const defs = svg.append("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) => {
g.attr("transform", event.transform);
});

// Store the zoom reference for the control buttons


zoomRef.current = zoom;

svg.call(zoom);

// Initial transform to center the graph - will be adjusted later with


fitGraphToScreen
svg.call(
zoom.transform,
d3.zoomIdentity.translate(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) => d.id)
.distance((d) => {
const isMainEntity =
MAIN_ENTITY_IDS.includes(d.source.id) ||
MAIN_ENTITY_IDS.includes(d.target.id);

if (
MAIN_ENTITY_IDS.includes(d.source.id) &&
MAIN_ENTITY_IDS.includes(d.target.id)
) {
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",
d3.forceX().strength((d) => {
// Stronger x-positioning for main entities
return MAIN_ENTITY_IDS.includes(d.id) ? 0.2 : 0.05;
})
)
// Reduce y-positioning force
.force(
"y",
d3.forceY().strength((d) => {
// Weaker y-positioning to allow horizontal spread
return MAIN_ENTITY_IDS.includes(d.id) ? 0.05 : 0.08;
})
)
.force("charge", d3.forceManyBody().strength(-1500)) // Stronger repulsion
.force(
"collision",
d3.forceCollide().radius((d) => {
return MAIN_ENTITY_IDS.includes(d.id)
? d.radius * 2.2 // Slightly increased
: d.radius * 1.8; // Slightly increased
})
)
.force("center", d3.forceCenter(width / 2, height / 2))
.alphaDecay(0.03) // Slower decay for better settling
.velocityDecay(0.5);

// Store the simulation reference


simulationRef.current = 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) =>
d.type === "debit" ? colorPalette.Debit : colorPalette.Credit
)
.attr("stroke-width", edgeThickness)
.attr("marker-end", (d) => `url(#arrow-${d.type})`)
.attr("d", (d) =>
lineGenerator([
{ x: d.source.x, y: d.source.y },
{ x: d.target.x, y: d.target.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 (event.defaultPrevented) return;

setSelectedNode({
id: d.id,
data: {
type: d.type,
isMainEntity: d.isMainEntity,
hasSelfTransfer: d.hasSelfTransfer,
selfTransferData: d.selfTransferData,
},
});
setIsDialogOpen(true);
});

node
.append("circle")
.attr("r", (d) => d.radius)
.attr("fill", (d) => d.color)
.attr("stroke", "#2C3E50")
.attr("stroke-width", (d) => (d.isMainEntity ? 4 : 2));

// FINAL SELF-TRANSFER LOOP AT TOP-CENTER (REFRESH STYLES)


node.each(function (d) {
if (d.hasSelfTransfer) {
const selfTransferData = d.selfTransferData;
const nodeRadius = d.radius;
const g = d3.select(this);

// Create a unique marker ID for this node to prevent conflicts


const markerId = `arrow-self-${d.id
.replace(/\s+/g, "-")
.replace(/[^\w-]/g, "")}`;

// Define marker in defs for the arrow head


const defs = svg.select("defs");
if (!defs.select("#" + markerId).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", colorPalette.SelfTransfer);
}

// 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 * Math.PI * 0.97); // Leave small gap for arrow (end at
~350 degrees)

// Create the main loop path


g.append("path")
.attr("d", arcGenerator)
.attr("transform", `translate(${loopCenterX},${loopCenterY})`)
.attr("fill", "none")
.attr("stroke", colorPalette.SelfTransfer)
.attr("stroke-width", 2.5)
.attr("marker-end", `url(#${markerId})`);
}
});

// Node Labels
// In the node.each function - around line 620
node.each(function (d) {
const nodeEl = d3.select(this);
const words = d.label.split(/\s+/);
const lineHeight = Math.max(12, d.radius * 0.25); // Scale lineHeight with
node radius
// Better font size calculation - proportional to node radius
const fontSize = Math.max(10, Math.min(16, d.radius * 0.3)); // Min 10px,
max 16px, otherwise proportional
const yOffset = d.radius * 0.1;

// Remove any previous labels to prevent duplication


nodeEl.selectAll(".node-label").remove();

const text = nodeEl


.append("text")
.attr("class", "node-label")
.attr("text-anchor", "middle")
.attr("dy", `-${((words.length - 1) * lineHeight) / 2}`)
.attr("fill", "#2C3E50")
.style("font-size", `${fontSize}px`)
.style("font-weight", "bold");

words.forEach((word, i) => {
text
.append("tspan")
.attr("x", 0)
.attr("y", yOffset + i * lineHeight)
.text(word);
});

// Also adjust the self-transfer amount font size


if (d.hasSelfTransfer && d.selfTransferData) {
nodeEl
.append("text")
.attr("class", "main-transfer-amount")
.attr("text-anchor", "middle")
.attr("x", 0)
.attr("y", yOffset + words.length * lineHeight + lineHeight / 2) //
Scale this offset too
.attr("fill", colorPalette.SelfTransfer)
.attr("font-size", `${fontSize * 0.9}px`) // Slightly smaller than
label
.attr("font-weight", "bold")
.text(`₹${d.selfTransferData.total.toLocaleString()}`);
}
});

// Create separate groups for debit and credit edge labels to better manage
them
const debitEdgeLabels = g
.append("g")
.selectAll("g")
.data(edges.filter((d) => d.type === "debit"))
.join("g");

const creditEdgeLabels = g
.append("g")
.selectAll("g")
.data(edges.filter((d) => d.type === "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", colorPalette.Debit)
.attr("stroke", "white")
.attr("stroke-width", "3px")
.attr("paint-order", "stroke")
.text((d) => `₹${d.value.toLocaleString()}`);

// 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", colorPalette.Credit)
.attr("stroke", "white")
.attr("stroke-width", "3px")
.attr("paint-order", "stroke")
.text((d) => `₹${d.value.toLocaleString()}`);

simulation.tick(100);
updatePositions();
simulation.stop();

// 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() {
node.attr("transform", (d) => `translate(${d.x},${d.y})`);

// Improved edge path creation with greater separation between debit and
credit
link.attr("d", (d) => {
const sourceNode = nodes.find((n) => n.id === d.source.id);
const targetNode = nodes.find((n) => n.id === d.target.id);

if (!sourceNode || !targetNode) return "";

const sourceRadius = sourceNode.radius;


const targetRadius = targetNode.radius;

const dx = d.target.x - d.source.x;


const dy = d.target.y - d.source.y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance === 0) return "";

const offsetSourceX = d.source.x + (dx / distance) * sourceRadius;


const offsetSourceY = d.source.y + (dy / distance) * sourceRadius;
const offsetTargetX = d.target.x - (dx / distance) * targetRadius;
const offsetTargetY = d.target.y - (dy / distance) * targetRadius;

const perpX = -dy / distance;


const perpY = dx / distance;

// Increased separation between debit and credit edges


const offsetMultiplier = d.type === "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


d.pathCoordinates = {
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
debitEdgeLabels.attr("transform", (d) => {
if (!d.source || !d.target || !d.pathCoordinates)
return "translate(0,0)";

const coords = d.pathCoordinates;

// 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 * coords.startX +
2 * t1 * t * coords.controlX +
t * t * coords.endX;
const pointY =
t1 * t1 * coords.startY +
2 * t1 * t * coords.controlY +
t * t * coords.endY;

// Calculate tangent direction to the curve at this point


const tangentX =
2 * (1 - t) * (coords.controlX - coords.startX) +
2 * t * (coords.endX - coords.controlX);
const tangentY =
2 * (1 - t) * (coords.controlY - coords.startY) +
2 * t * (coords.endY - coords.controlY);

// Normalize tangent vector


const tangentLength = Math.sqrt(
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
creditEdgeLabels.attr("transform", (d) => {
if (!d.source || !d.target || !d.pathCoordinates)
return "translate(0,0)";

const coords = d.pathCoordinates;

// 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 * coords.startX +
2 * t1 * t * coords.controlX +
t * t * coords.endX;
const pointY =
t1 * t1 * coords.startY +
2 * t1 * t * coords.controlY +
t * t * coords.endY;

// Calculate tangent direction to the curve at this point


const tangentX =
2 * (1 - t) * (coords.controlX - coords.startX) +
2 * t * (coords.endX - coords.controlX);
const tangentY =
2 * (1 - t) * (coords.controlY - coords.startY) +
2 * t * (coords.endY - coords.controlY);

// Normalize tangent vector


const tangentLength = Math.sqrt(
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 (!event.active) simulationRef.current.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;

// Prevent click from firing


event.sourceEvent.stopPropagation();
}

function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;

// Update positions during drag for smoother experience


updatePositions();
}

// Update dragended function - around line 790


function dragended(event, d) {
if (!event.active) simulationRef.current.alphaTarget(0);
updatePositions();
// NEW: Ensure graph stays in view after dragging operations
setTimeout(fitGraphToScreen, 100);
}
} catch (error) {
console.error("Error rendering network graph:", error);

const width = graphContainerRef.current.clientWidth;


const height = 600;

const svg = d3
.select(svgRef.current)
.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 = React.useMemo(() => {


if (!selectedNode) return [];

return data.filter(
(tx) => tx.name === selectedNode.id || tx.entity === selectedNode.id
);
}, [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(e.target.value || 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;

You might also like