CharacterGraph.jsx 4.0 KB

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