CharacterGraph.jsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { useState, useEffect, useRef } from "react";
  2. import ForceGraph2D from "react-force-graph-2d";
  3. // import * as d3 from 'd3';
  4. export default function CharacterGraph({ graphData }) {
  5. const containerRef = useRef(null);
  6. const [dimensions, setDimensions] = useState({ width: 600, height: 600 });
  7. const [hoveredLink, setHoveredLink] = useState(null);
  8. const fgRef = useRef();
  9. useEffect(() => {
  10. const updateDimensions = () => {
  11. if (containerRef.current) {
  12. setDimensions({
  13. width: containerRef.current.offsetWidth,
  14. height: Math.max(300, containerRef.current.offsetHeight),
  15. });
  16. }
  17. };
  18. updateDimensions();
  19. window.addEventListener("resize", updateDimensions);
  20. return () => window.removeEventListener("resize", updateDimensions);
  21. }, []);
  22. return (
  23. <div className="bg-white shadow-lg rounded-lg p-6">
  24. <h2 className="text-xl font-semibold text-gray-800 mb-4">
  25. Character Relationship Graph
  26. </h2>
  27. <div ref={containerRef} className="w-full h-[600px]">
  28. <ForceGraph2D
  29. ref={fgRef}
  30. graphData={graphData}
  31. nodeCanvasObject={(node, ctx, globalScale) => {
  32. // Draw node
  33. ctx.beginPath();
  34. ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI);
  35. ctx.fillStyle = "transparent";
  36. ctx.fill();
  37. // Always show node label
  38. const label = node.name;
  39. const fontSize = 20 / globalScale;
  40. ctx.font = `${fontSize}px Arial`;
  41. const textWidth = ctx.measureText(label).width;
  42. const bckgDimensions = [textWidth, fontSize].map(
  43. (n) => n + fontSize * 0.2
  44. );
  45. ctx.textAlign = "center";
  46. ctx.textBaseline = "middle";
  47. ctx.fillStyle = "#3b82f6";
  48. ctx.fillText(label, node.x, node.y);
  49. node.__bckgDimensions = bckgDimensions;
  50. }}
  51. onLinkHover={(link) => setHoveredLink(link ? `${link.source.id}-${link.target.id}` : null)}
  52. nodePointerAreaPaint={(node, color, ctx) => {
  53. ctx.fillStyle = color;
  54. const bckgDimensions = node.__bckgDimensions;
  55. bckgDimensions &&
  56. ctx.fillRect(
  57. node.x - bckgDimensions[0] / 2,
  58. node.y - bckgDimensions[1] / 2,
  59. ...bckgDimensions
  60. );
  61. }}
  62. linkCanvasObject={(link, ctx, globalScale) => {
  63. const start = link.source;
  64. const end = link.target;
  65. ctx.beginPath();
  66. ctx.moveTo(start.x, start.y);
  67. ctx.lineTo(end.x, end.y);
  68. ctx.strokeStyle = "#9ca3af";
  69. ctx.lineWidth = 0.5;
  70. ctx.stroke();
  71. // Only draw label if link is hovered
  72. const linkId = `${link.source.id}-${link.target.id}`;
  73. if (hoveredLink === linkId && link.label) {
  74. const textPos = {
  75. x: start.x + (end.x - start.x) / 2,
  76. y: start.y + (end.y - start.y) / 2
  77. };
  78. const fontSize = 3 + 1/globalScale;
  79. ctx.font = `${fontSize}px Arial`;
  80. const textWidth = ctx.measureText(link.label).width;
  81. const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2);
  82. ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
  83. ctx.fillRect(
  84. textPos.x - bckgDimensions[0] / 2,
  85. textPos.y - bckgDimensions[1] / 2,
  86. ...bckgDimensions
  87. );
  88. ctx.textAlign = 'center';
  89. ctx.textBaseline = 'middle';
  90. ctx.fillStyle = '#666';
  91. ctx.fillText(link.label, textPos.x, textPos.y);
  92. }
  93. }}
  94. onNodeDragEnd={node => {
  95. node.fx = node.x;
  96. node.fy = node.y;
  97. }}
  98. linkDirectionalArrowLength={3.5}
  99. linkDirectionalArrowRelPos={3}
  100. linkWidth={1}
  101. backgroundColor="#ffffff"
  102. width={dimensions.width}
  103. height={dimensions.height}
  104. onEngineStop={() => fgRef.current.zoomToFit(600)}
  105. cooldownTicks={100}
  106. />
  107. </div>
  108. </div>
  109. );
  110. }