Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 56 additions & 56 deletions packages/web/src/components/InteractiveGraphVisualization.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import * as d3 from 'd3';
import { Link2, Edit3, Trash2, Eye, Clock, User, Tag } from 'lucide-react';
import { Link2, Edit3, Trash2, Eye, Clock, User, Tag, Plus, Minus } from 'lucide-react';
import { useGraph } from '../contexts/GraphContext';
import { mockProjectNodes, mockProjectEdges, relationshipTypeInfo, MockNode, MockEdge, RelationshipType } from '../types/projectData';

Expand All @@ -19,6 +19,8 @@ interface EdgeMenuState {
export function InteractiveGraphVisualization() {
const svgRef = useRef<SVGSVGElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const graphSwitcherRef = useRef<HTMLDivElement>(null);
const zoomBehaviorRef = useRef<d3.ZoomBehavior<SVGSVGElement, unknown> | null>(null);
const { currentGraph, availableGraphs, selectGraph } = useGraph();

const [nodeMenu, setNodeMenu] = useState<NodeMenuState>({ node: null, position: { x: 0, y: 0 }, visible: false });
Expand Down Expand Up @@ -252,6 +254,7 @@ export function InteractiveGraphVisualization() {
const zoom = d3.zoom<SVGSVGElement, unknown>()
.scaleExtent([0.1, 4]);

zoomBehaviorRef.current = zoom;
svg.call(zoom);

const g = svg.append('g');
Expand Down Expand Up @@ -729,57 +732,7 @@ export function InteractiveGraphVisualization() {
updateHtmlLabels();
});

// Add zoom controls
const zoomControls = svg.append('g')
.attr('class', 'zoom-controls')
.attr('transform', 'translate(20, 20)');

const zoomIn = zoomControls.append('g')
.attr('class', 'zoom-button')
.style('cursor', 'pointer')
.on('click', () => {
svg.transition().duration(300).call(zoom.scaleBy, 1.5);
});

zoomIn.append('rect')
.attr('width', 30)
.attr('height', 30)
.attr('fill', '#374151')
.attr('stroke', '#6b7280')
.attr('rx', 4);

zoomIn.append('text')
.attr('x', 15)
.attr('y', 20)
.attr('text-anchor', 'middle')
.attr('font-size', '16px')
.attr('font-weight', 'bold')
.attr('fill', 'white')
.text('+');

const zoomOut = zoomControls.append('g')
.attr('class', 'zoom-button')
.attr('transform', 'translate(0, 35)')
.style('cursor', 'pointer')
.on('click', () => {
svg.transition().duration(300).call(zoom.scaleBy, 0.67);
});

zoomOut.append('rect')
.attr('width', 30)
.attr('height', 30)
.attr('fill', '#374151')
.attr('stroke', '#6b7280')
.attr('rx', 4);

zoomOut.append('text')
.attr('x', 15)
.attr('y', 20)
.attr('text-anchor', 'middle')
.attr('font-size', '16px')
.attr('font-weight', 'bold')
.attr('fill', 'white')
.text('−');
// Zoom controls are now handled by React components

}, [handleNodeClick, handleEdgeClick]);

Expand All @@ -794,17 +747,36 @@ export function InteractiveGraphVisualization() {
return () => window.removeEventListener('resize', handleResize);
}, [initializeVisualization]);

// Close graph switcher when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (graphSwitcherRef.current && !graphSwitcherRef.current.contains(event.target as Node)) {
setShowGraphSwitcher(false);
}
};

if (showGraphSwitcher) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [showGraphSwitcher]);

return (
<div ref={containerRef} className="graph-container relative w-full h-full bg-gray-900">
<svg ref={svgRef} className="w-full h-full" style={{ background: 'radial-gradient(circle at center, #1f2937 0%, #111827 100%)' }} />
<svg
ref={svgRef}
className="w-full h-full"
style={{ background: 'radial-gradient(circle at center, #1f2937 0%, #111827 100%)' }}
onClick={() => setShowGraphSwitcher(false)}
/>

{/* Graph Switcher Trigger */}
<div
ref={graphSwitcherRef}
className="absolute top-4 left-4 z-40"
onMouseEnter={() => setShowGraphSwitcher(true)}
onMouseLeave={() => setShowGraphSwitcher(false)}
>
<button
onClick={() => setShowGraphSwitcher(!showGraphSwitcher)}
className="bg-gray-800/90 backdrop-blur-sm border border-gray-600 rounded-lg px-3 py-2 shadow-md hover:bg-gray-700 transition-all duration-200"
>
<div className="flex items-center space-x-2">
Expand All @@ -825,7 +797,7 @@ export function InteractiveGraphVisualization() {
<h3 className="text-sm font-medium text-green-300">Switch Graph</h3>
<p className="text-xs text-gray-400 mt-1">Select a different graph to visualize</p>
</div>
<div className="max-h-64 overflow-y-auto">
<div className="max-h-64 overflow-y-auto scrollbar-gray">
{availableGraphs.map((graph) => (
<button
key={graph.id}
Expand Down Expand Up @@ -874,6 +846,34 @@ export function InteractiveGraphVisualization() {
)}
</div>

{/* Zoom Controls - Top Right */}
<div className="absolute top-4 right-4 z-40 flex space-x-2">
<button
onClick={() => {
if (zoomBehaviorRef.current && svgRef.current) {
const svg = d3.select(svgRef.current);
svg.transition().duration(300).call(zoomBehaviorRef.current.scaleBy, 1.5);
}
}}
className="flex items-center justify-center w-8 h-8 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors border border-gray-600"
title="Zoom In"
>
<Plus className="h-4 w-4" />
</button>
<button
onClick={() => {
if (zoomBehaviorRef.current && svgRef.current) {
const svg = d3.select(svgRef.current);
svg.transition().duration(300).call(zoomBehaviorRef.current.scaleBy, 0.67);
}
}}
className="flex items-center justify-center w-8 h-8 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors border border-gray-600"
title="Zoom Out"
>
<Minus className="h-4 w-4" />
</button>
</div>

{/* Graph Controls */}
<div className="absolute bottom-4 right-4 space-y-2">
<button
Expand Down
Loading