"""
Span Link Layout
Generates the UI for creating and managing relationships/links between spans.
This schema type works in conjunction with a span annotation schema to allow
users to annotate relationships like "PERSON works_for ORGANIZATION".
"""
import logging
from .identifier_utils import (
safe_generate_layout,
generate_element_identifier,
escape_html_content,
generate_tooltip_html
)
from .span import get_span_color, SPAN_COLOR_PALETTE
logger = logging.getLogger(__name__)
# Default colors for link types
LINK_COLOR_PALETTE = [
"#dc2626", # Red
"#22c55e", # Green
"#a855f7", # Purple
"#f59e0b", # Amber
"#3b82f6", # Blue
"#ec4899", # Pink
"#06b6d4", # Cyan
"#f97316", # Orange
"#8b5cf6", # Violet
"#10b981", # Emerald
]
def _generate_span_link_layout_internal(annotation_scheme, horizontal=False):
"""
Internal function to generate span link layout after validation.
Args:
annotation_scheme: Configuration dictionary containing:
- name: Schema name
- description: Description shown to user
- span_schema: Name of the span schema to link
- link_types: List of link type definitions with:
- name: Link type name (e.g., "WORKS_FOR")
- directed: Whether the link is directed (default: false)
- allowed_source_labels: Optional list of allowed source span labels
- allowed_target_labels: Optional list of allowed target span labels
- max_spans: Maximum spans in a link (default: 2, higher for n-ary)
- color: Optional color for this link type
horizontal: Whether to display horizontally (not used for links)
Returns:
tuple: (HTML string, key bindings list)
"""
scheme_name = annotation_scheme["name"]
description = annotation_scheme.get("description", "Create relationships between spans")
span_schema = annotation_scheme.get("span_schema", "")
link_types = annotation_scheme.get("link_types", [])
visual_display = annotation_scheme.get("visual_display", {})
# Build link types HTML
link_types_html = ""
key_bindings = []
for i, link_type in enumerate(link_types):
link_name = link_type.get("name", f"Link_{i}")
directed = link_type.get("directed", False)
max_spans = link_type.get("max_spans", 2)
color = link_type.get("color", LINK_COLOR_PALETTE[i % len(LINK_COLOR_PALETTE)])
# Direction indicator
direction_icon = "→" if directed else "↔"
direction_class = "directed" if directed else "undirected"
# Tooltip with constraints
tooltip_parts = []
if link_type.get("allowed_source_labels"):
tooltip_parts.append(f"Source: {', '.join(link_type['allowed_source_labels'])}")
if link_type.get("allowed_target_labels"):
tooltip_parts.append(f"Target: {', '.join(link_type['allowed_target_labels'])}")
if max_spans > 2:
tooltip_parts.append(f"N-ary: up to {max_spans} spans")
tooltip_attr = ""
if tooltip_parts:
tooltip_text = "; ".join(tooltip_parts)
tooltip_attr = f'data-toggle="tooltip" data-placement="top" title="{escape_html_content(tooltip_text)}"'
link_types_html += f"""
Click on highlighted spans to select them for linking
No links created yet
"""
return schematic, key_bindings
def generate_span_link_layout(annotation_scheme, horizontal=False):
"""
Generate span link layout HTML for the given annotation scheme.
Args:
annotation_scheme (dict): The annotation scheme configuration
horizontal (bool): Whether to display horizontally
Returns:
tuple: (HTML string, key bindings list)
"""
return safe_generate_layout(annotation_scheme, _generate_span_link_layout_internal, horizontal)
def render_link_arcs(links, span_positions):
"""
Generate SVG arcs for visualizing links between spans.
Args:
links: List of SpanLink objects
span_positions: Dictionary mapping span_id -> {x, y, width, height} positions
Returns:
str: SVG markup for the link arcs
"""
if not links or not span_positions:
return ""
svg_paths = []
for link in links:
span_ids = link.get_span_ids()
if len(span_ids) < 2:
continue
# Get color for this link type
link_color = link.get_properties().get("color", "#dc2626")
# For binary links, draw a simple arc
if len(span_ids) == 2:
span1_id, span2_id = span_ids
if span1_id not in span_positions or span2_id not in span_positions:
continue
pos1 = span_positions[span1_id]
pos2 = span_positions[span2_id]
# Calculate arc endpoints (center of each span)
x1 = pos1["x"] + pos1["width"] / 2
y1 = pos1["y"]
x2 = pos2["x"] + pos2["width"] / 2
y2 = pos2["y"]
# Calculate control point for the arc
mid_x = (x1 + x2) / 2
arc_height = min(abs(x2 - x1) / 3, 50) # Arc height proportional to distance
# Create SVG path
if link.is_directed():
# Directed link with arrow
svg_paths.append(f"""
""")
else:
# Undirected link
svg_paths.append(f"""
""")
# For n-ary links, connect all spans to a central point
else:
# Calculate center point
valid_positions = [span_positions[sid] for sid in span_ids if sid in span_positions]
if len(valid_positions) < 2:
continue
center_x = sum(p["x"] + p["width"] / 2 for p in valid_positions) / len(valid_positions)
center_y = min(p["y"] for p in valid_positions) - 30 # Above the spans
# Draw lines from each span to center
for span_id in span_ids:
if span_id not in span_positions:
continue
pos = span_positions[span_id]
x = pos["x"] + pos["width"] / 2
y = pos["y"]
svg_paths.append(f"""
""")
# Draw central node
svg_paths.append(f"""
""")
# Wrap in SVG with defs for arrow markers
svg = f"""
"""
return svg