Improve label staggering and remove parallel badge

- Simplify label positioning formula: symmetric 10% stagger per offset unit
- Remove parallel edge badge (X relations) - not needed since all labels show
- Labels now closer to center with consistent formula
- Better balance between separation and readability

Example for 3 edges:
- offset -1: label at t=0.4 (toward source)
- offset 0: label at t=0.5 (center)
- offset +1: label at t=0.6 (toward target)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik 2026-02-05 14:14:22 +01:00
parent fdef63e8bd
commit ce7f6c2eed
3 changed files with 19 additions and 46 deletions

View file

@ -96,18 +96,25 @@ const CustomEdge = ({
// Create cubic bezier path using custom control points // Create cubic bezier path using custom control points
const edgePath = `M ${params.sx},${params.sy} C ${params.sourceControlX},${params.sourceControlY} ${params.targetControlX},${params.targetControlY} ${params.tx},${params.ty}`; const edgePath = `M ${params.sx},${params.sy} C ${params.sourceControlX},${params.sourceControlY} ${params.targetControlX},${params.targetControlY} ${params.tx},${params.ty}`;
// Calculate label position at midpoint of the bezier curve (t=0.5) // Smart label positioning: vary position along curve based on offset
const t = 0.5; // This staggers labels for parallel edges to reduce overlap
// Symmetric staggering: negative offsets toward source, positive toward target
const labelPositionOffset = offsetMultiplier * 0.1; // 10% stagger per offset unit
const t = 0.5 + labelPositionOffset;
// Clamp t to reasonable range to keep labels on the visible part of the curve
const clampedT = Math.max(0.3, Math.min(0.7, t));
const labelX = const labelX =
Math.pow(1 - t, 3) * params.sx + Math.pow(1 - clampedT, 3) * params.sx +
3 * Math.pow(1 - t, 2) * t * params.sourceControlX + 3 * Math.pow(1 - clampedT, 2) * clampedT * params.sourceControlX +
3 * (1 - t) * Math.pow(t, 2) * params.targetControlX + 3 * (1 - clampedT) * Math.pow(clampedT, 2) * params.targetControlX +
Math.pow(t, 3) * params.tx; Math.pow(clampedT, 3) * params.tx;
const labelY = const labelY =
Math.pow(1 - t, 3) * params.sy + Math.pow(1 - clampedT, 3) * params.sy +
3 * Math.pow(1 - t, 2) * t * params.sourceControlY + 3 * Math.pow(1 - clampedT, 2) * clampedT * params.sourceControlY +
3 * (1 - t) * Math.pow(t, 2) * params.targetControlY + 3 * (1 - clampedT) * Math.pow(clampedT, 2) * params.targetControlY +
Math.pow(t, 3) * params.ty; Math.pow(clampedT, 3) * params.ty;
return { edgePath, labelX, labelY }; return { edgePath, labelX, labelY };
}, [sourceNode, targetNode, sourceShape, targetShape, sourceX, sourceY, targetX, targetY, data]); }, [sourceNode, targetNode, sourceShape, targetShape, sourceX, sourceY, targetX, targetY, data]);
@ -117,10 +124,6 @@ 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);
@ -232,8 +235,8 @@ const CustomEdge = ({
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
/> />
{/* Edge label - show custom or type default, plus labels, plus aggregation count, plus parallel badge */} {/* Edge label - show custom or type default, plus labels, plus aggregation count */}
{(displayLabel || (data?.labels && data.labels.length > 0) || (data as { aggregatedCount?: number })?.aggregatedCount || showParallelBadge) && ( {(displayLabel || (data?.labels && data.labels.length > 0) || (data as { aggregatedCount?: number })?.aggregatedCount) && (
<EdgeLabelRenderer> <EdgeLabelRenderer>
<div <div
style={{ style={{
@ -275,16 +278,6 @@ 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>
)} )}

View file

@ -432,19 +432,11 @@ const GraphEditor = ({ presentationMode = false, onNodeSelect, onEdgeSelect, onG
) { ) {
const currentViewport = getCurrentViewport(); const currentViewport = getCurrentViewport();
saveViewport(prevDocumentIdRef.current, currentViewport); saveViewport(prevDocumentIdRef.current, currentViewport);
console.log(
`Saved viewport for document: ${prevDocumentIdRef.current}`,
currentViewport,
);
} }
// Restore viewport for the new document // Restore viewport for the new document
const savedViewport = getViewport(activeDocumentId); const savedViewport = getViewport(activeDocumentId);
if (savedViewport) { if (savedViewport) {
console.log(
`Restoring viewport for document: ${activeDocumentId}`,
savedViewport,
);
setViewport(savedViewport, { duration: 0 }); setViewport(savedViewport, { duration: 0 });
} }

View file

@ -574,18 +574,6 @@ export function getFloatingEdgeParams(
const targetControlX = targetIntersection.x + Math.cos(targetIntersection.angle) * controlPointDistance + perpOffset.x; const targetControlX = targetIntersection.x + Math.cos(targetIntersection.angle) * controlPointDistance + perpOffset.x;
const targetControlY = targetIntersection.y + Math.sin(targetIntersection.angle) * controlPointDistance + perpOffset.y; const targetControlY = targetIntersection.y + Math.sin(targetIntersection.angle) * controlPointDistance + perpOffset.y;
// Debug: Log control point offsets
if (offsetMultiplier !== 0) {
console.log('🎯 Edge path with offset:', {
offsetMultiplier,
perpOffset,
controlPointDistance,
endpointOffset: sourceEndpointOffset,
sourceControl: { x: sourceControlX, y: sourceControlY },
targetControl: { x: targetControlX, y: targetControlY },
});
}
return { return {
sx: sourceIntersection.x + sourceEndpointOffset.x, sx: sourceIntersection.x + sourceEndpointOffset.x,
sy: sourceIntersection.y + sourceEndpointOffset.y, sy: sourceIntersection.y + sourceEndpointOffset.y,