mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-03-13 12:08:46 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
3daedbc0d8
commit
833c130690
1 changed files with 35 additions and 4 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo, useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
EdgeProps,
|
EdgeProps,
|
||||||
EdgeLabelRenderer,
|
EdgeLabelRenderer,
|
||||||
|
|
@ -42,6 +42,17 @@ const CustomEdge = ({
|
||||||
// Get active filters based on mode (editing vs presentation)
|
// Get active filters based on mode (editing vs presentation)
|
||||||
const filters = useActiveFilters();
|
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
|
// Get internal nodes for floating edge calculations with correct absolute positioning
|
||||||
const sourceNode = useInternalNode(source);
|
const sourceNode = useInternalNode(source);
|
||||||
const targetNode = useInternalNode(target);
|
const targetNode = useInternalNode(target);
|
||||||
|
|
@ -105,6 +116,10 @@ const CustomEdge = ({
|
||||||
// Check if this is an aggregated edge
|
// Check if this is an aggregated edge
|
||||||
const isAggregated = !!(data as { aggregatedCount?: number })?.aggregatedCount;
|
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
|
// Find the edge type configuration
|
||||||
const edgeTypeConfig = edgeTypes.find((et) => et.id === data?.type);
|
const edgeTypeConfig = edgeTypes.find((et) => et.id === data?.type);
|
||||||
|
|
||||||
|
|
@ -151,6 +166,9 @@ const CustomEdge = ({
|
||||||
// Calculate opacity based on visibility
|
// Calculate opacity based on visibility
|
||||||
const edgeOpacity = hasActiveFilters && !isMatch ? 0.2 : 1.0;
|
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)
|
// Create unique marker IDs based on color (for reusability)
|
||||||
const safeColor = edgeColor.replace('#', '');
|
const safeColor = edgeColor.replace('#', '');
|
||||||
const markerEndId = `arrow-end-${safeColor}`;
|
const markerEndId = `arrow-end-${safeColor}`;
|
||||||
|
|
@ -202,16 +220,19 @@ const CustomEdge = ({
|
||||||
path={edgePath}
|
path={edgePath}
|
||||||
style={{
|
style={{
|
||||||
stroke: edgeColor,
|
stroke: edgeColor,
|
||||||
strokeWidth: selected ? 3 : 2,
|
strokeWidth,
|
||||||
strokeDasharray,
|
strokeDasharray,
|
||||||
opacity: edgeOpacity,
|
opacity: edgeOpacity,
|
||||||
|
transition: 'stroke-width 150ms ease-in-out',
|
||||||
}}
|
}}
|
||||||
markerEnd={markerEnd}
|
markerEnd={markerEnd}
|
||||||
markerStart={markerStart}
|
markerStart={markerStart}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Edge label - show custom or type default, plus labels, plus aggregation count */}
|
{/* 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) && (
|
{(displayLabel || (data?.labels && data.labels.length > 0) || (data as { aggregatedCount?: number })?.aggregatedCount || showParallelBadge) && (
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -253,6 +274,16 @@ const CustomEdge = ({
|
||||||
{(data as { aggregatedCount?: number }).aggregatedCount} relations
|
{(data as { aggregatedCount?: number }).aggregatedCount} relations
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Parallel edge badge for 4+ parallel edges (show only on center edge) */}
|
||||||
|
{showParallelBadge && Math.abs((data as { offsetMultiplier?: number })?.offsetMultiplier || 0) < 0.1 && (
|
||||||
|
<div
|
||||||
|
className="mt-1 px-2 py-0.5 rounded-full text-xs font-semibold text-white"
|
||||||
|
style={{ backgroundColor: '#6b7280' }}
|
||||||
|
title={`${parallelGroupSize} parallel relations between these nodes`}
|
||||||
|
>
|
||||||
|
{parallelGroupSize} relations
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</EdgeLabelRenderer>
|
</EdgeLabelRenderer>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue