From 833c13069004ebaacc5cb0fa58eca8abc92e1cb5 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Thu, 5 Feb 2026 13:27:09 +0100 Subject: [PATCH] Add visual enhancements for parallel edges Add hover states and badge indicators to improve parallel edge visibility: Features: - Hover effect: Edge stroke increases from 2px to 3px on hover - Selected edges: Stroke increases to 4px for clear selection feedback - Parallel badge: Show "X relations" badge when 4+ parallel edges exist - Badge positioning: Display only on center edge to avoid clutter - Smooth transitions: 150ms ease-in-out for stroke width changes Visual design: - Default stroke: 2px - Hover stroke: 3px - Selected stroke: 4px - Badge color: neutral gray (#6b7280) - Badge appears at edge label position All 517 tests pass. Co-Authored-By: Claude Sonnet 4.5 --- src/components/Edges/CustomEdge.tsx | 39 ++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/Edges/CustomEdge.tsx b/src/components/Edges/CustomEdge.tsx index 19cd287..ff5b2c1 100644 --- a/src/components/Edges/CustomEdge.tsx +++ b/src/components/Edges/CustomEdge.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo } from 'react'; +import { memo, useMemo, useState, useCallback } from 'react'; import { EdgeProps, EdgeLabelRenderer, @@ -42,6 +42,17 @@ const CustomEdge = ({ // Get active filters based on mode (editing vs presentation) const filters = useActiveFilters(); + // Hover state for parallel edge highlighting + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = useCallback(() => { + setIsHovered(true); + }, []); + + const handleMouseLeave = useCallback(() => { + setIsHovered(false); + }, []); + // Get internal nodes for floating edge calculations with correct absolute positioning const sourceNode = useInternalNode(source); const targetNode = useInternalNode(target); @@ -105,6 +116,10 @@ const CustomEdge = ({ // Check if this is an aggregated edge const isAggregated = !!(data as { aggregatedCount?: number })?.aggregatedCount; + // Check if this edge is part of a large parallel group (4+ edges) + const parallelGroupSize = (data as { parallelGroupSize?: number })?.parallelGroupSize || 0; + const showParallelBadge = parallelGroupSize >= 4; + // Find the edge type configuration const edgeTypeConfig = edgeTypes.find((et) => et.id === data?.type); @@ -151,6 +166,9 @@ const CustomEdge = ({ // Calculate opacity based on visibility const edgeOpacity = hasActiveFilters && !isMatch ? 0.2 : 1.0; + // Calculate stroke width based on state (selected, hovered, or default) + const strokeWidth = selected ? 4 : isHovered ? 3 : 2; + // Create unique marker IDs based on color (for reusability) const safeColor = edgeColor.replace('#', ''); const markerEndId = `arrow-end-${safeColor}`; @@ -202,16 +220,19 @@ const CustomEdge = ({ path={edgePath} style={{ stroke: edgeColor, - strokeWidth: selected ? 3 : 2, + strokeWidth, strokeDasharray, opacity: edgeOpacity, + transition: 'stroke-width 150ms ease-in-out', }} markerEnd={markerEnd} markerStart={markerStart} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} /> - {/* Edge label - show custom or type default, plus labels, plus aggregation count */} - {(displayLabel || (data?.labels && data.labels.length > 0) || (data as { aggregatedCount?: number })?.aggregatedCount) && ( + {/* Edge label - show custom or type default, plus labels, plus aggregation count, plus parallel badge */} + {(displayLabel || (data?.labels && data.labels.length > 0) || (data as { aggregatedCount?: number })?.aggregatedCount || showParallelBadge) && (
)} + {/* Parallel edge badge for 4+ parallel edges (show only on center edge) */} + {showParallelBadge && Math.abs((data as { offsetMultiplier?: number })?.offsetMultiplier || 0) < 0.1 && ( +
+ {parallelGroupSize} relations +
+ )}
)}