Status: Design Document
Version: 1.0
Date: December 26, 2025
This document describes EdgeCraft's advanced edge rendering capabilities, designed to support sophisticated graph visualization patterns common in RDF ontologies, knowledge graphs, and complex network analysis. Our implementation goes beyond simple directed edges to support:
- Directional Arrows - Configurable arrow placement (forward, backward, bidirectional)
- Self-Loops - Curved edges connecting a node to itself
- Multi-Edges - Multiple edges between the same node pair
- Bidirectional Relationships - Simultaneous forward and backward edges
- RDF Inverse Relationships - Interactive glyphs representing both sides of inverse properties
These features position EdgeCraft as the premier open-source library for ontology and knowledge graph visualization, with capabilities matching or exceeding commercial tools like Keylines.
Current State: EdgeCraft draws undirected edges by default.
Challenge: Many graphs are inherently directed:
- Social networks (Alice follows Bob)
- Dependencies (Package A depends on Package B)
- RDF triples (Subject → Predicate → Object)
- Process flows (Step 1 → Step 2)
User Need: Clear visual indication of edge direction with configurable arrow placement.
Current State: Edges assume distinct source and target nodes.
Challenge: Nodes often have relationships with themselves:
- File system loops (symlinks)
- Social networks (self-endorsement)
- RDF reflexive properties (rdfs:subClassOf can be reflexive)
User Need: Curved loop edges that don't obscure the node.
Current State: Multiple edges overlap, appearing as a single edge.
Challenge: Two nodes may have multiple distinct relationships:
- Person A:
knows,worksAt,marriedTo→ Person B - Package A:
depends,suggests,conflicts→ Package B - RDF: Multiple predicates between same subject/object
User Need: Visually distinct curved paths for each edge, avoiding overlaps.
Current State: Forward and reverse edges render as two separate lines.
Challenge: Some relationships are inherently bidirectional:
- Friendship (symmetric)
- Co-authorship
- Network connections (bidirectional data flow)
User Need: Single edge with arrows on both ends, or parallel edges with opposite directions.
Current State: RDF inverse properties render as separate edges or require manual handling.
Challenge: OWL/RDF ontologies define inverse object properties:
:hasPart owl:inverseOf :partOf .
:parent owl:inverseOf :child .
:locatedIn owl:inverseOf :contains .When we have:
:Car123 :hasPart :Engine456 .We can infer:
:Engine456 :partOf :Car123 .User Need:
- Visualize both directions of the relationship on a single edge
- Show both predicates (hasPart AND partOf) with their proper directionality
- Allow interaction with each side independently
- Normalize multi-directional relationships into coherent visual representation
This is what separates EdgeCraft from competitors - elegant abstractions for complex RDF semantics.
// Enhanced EdgeStyle with directional controls
interface EdgeStyle {
// Existing properties
stroke?: string;
strokeWidth?: number;
// NEW: Arrow configuration
arrow?: ArrowConfig;
// NEW: Self-loop configuration
selfLoop?: SelfLoopConfig;
// NEW: Multi-edge handling
parallelOffset?: number;
// NEW: RDF inverse relationship glyphs
glyphs?: EdgeGlyphConfig[];
}
interface ArrowConfig {
// Arrow placement
position: 'forward' | 'backward' | 'both' | 'none';
// Arrow style
size?: number; // Arrow head size
shape?: 'triangle' | 'chevron' | 'diamond' | 'circle';
filled?: boolean; // Filled or outline
// Advanced: offset from node (for multi-edges)
offset?: number; // Distance from target node
}
interface SelfLoopConfig {
// Loop geometry
radius?: number; // Size of the loop
angle?: number; // Position around node (0-360°)
// Visual style
clockwise?: boolean; // Direction of curve
}
interface EdgeGlyphConfig {
// Position on edge
position: number; // 0.0 to 1.0 along edge (0.5 = midpoint)
// Glyph content
text?: string; // Short label
icon?: string; // Unicode icon or emoji
shape?: 'circle' | 'square' | 'diamond';
// Visual style
size?: number;
fill?: string;
stroke?: string;
// Interaction
interactive?: boolean; // Can be clicked/hovered
tooltip?: string; // Hover tooltip
// RDF-specific: direction indicator
direction?: 'forward' | 'backward'; // Which way does this glyph point?
}class CanvasRenderer {
private renderEdge(ctx: CanvasRenderingContext2D, edge: GraphEdge, style: EdgeStyle): void {
const source = this.getNodePosition(edge.source);
const target = this.getNodePosition(edge.target);
// Draw edge line
ctx.beginPath();
ctx.moveTo(source.x, source.y);
ctx.lineTo(target.x, target.y);
ctx.stroke();
// Draw arrows
if (style.arrow) {
if (style.arrow.position === 'forward' || style.arrow.position === 'both') {
this.drawArrow(ctx, source, target, style.arrow);
}
if (style.arrow.position === 'backward' || style.arrow.position === 'both') {
this.drawArrow(ctx, target, source, style.arrow);
}
}
}
private drawArrow(
ctx: CanvasRenderingContext2D,
from: Position,
to: Position,
config: ArrowConfig
): void {
const size = config.size || 10;
const angle = Math.atan2(to.y - from.y, to.x - from.x);
// Calculate arrow tip position (offset from target)
const offset = config.offset || 0;
const tipX = to.x - Math.cos(angle) * offset;
const tipY = to.y - Math.sin(angle) * offset;
// Arrow wings
const leftAngle = angle + Math.PI - Math.PI / 6;
const rightAngle = angle + Math.PI + Math.PI / 6;
const leftX = tipX + Math.cos(leftAngle) * size;
const leftY = tipY + Math.sin(leftAngle) * size;
const rightX = tipX + Math.cos(rightAngle) * size;
const rightY = tipY + Math.sin(rightAngle) * size;
// Draw arrow
ctx.beginPath();
ctx.moveTo(tipX, tipY);
ctx.lineTo(leftX, leftY);
ctx.lineTo(rightX, rightY);
ctx.closePath();
if (config.filled !== false) {
ctx.fill();
}
ctx.stroke();
}
}// Fragment shader for arrow heads
#version 300 es
precision mediump float;
in vec2 v_texCoord;
in vec4 v_color;
in float v_arrowType; // 0=none, 1=forward, 2=backward, 3=both
out vec4 fragColor;
void main() {
// Calculate if this fragment is part of an arrow
float dist = length(v_texCoord);
// Arrow shape using distance field
float arrowShape = 0.0;
if (v_arrowType == 1.0 || v_arrowType == 3.0) {
// Forward arrow
vec2 p = v_texCoord;
float d = abs(p.y) - p.x * 0.5;
arrowShape = max(arrowShape, 1.0 - smoothstep(0.0, 0.1, d));
}
if (v_arrowType == 2.0 || v_arrowType == 3.0) {
// Backward arrow
vec2 p = vec2(-v_texCoord.x, v_texCoord.y);
float d = abs(p.y) - p.x * 0.5;
arrowShape = max(arrowShape, 1.0 - smoothstep(0.0, 0.1, d));
}
fragColor = v_color * arrowShape;
}function calculateSelfLoopPath(
nodePos: Position,
nodeRadius: number,
config: SelfLoopConfig
): BezierPath {
const loopRadius = config.radius || 40;
const angle = (config.angle || 0) * Math.PI / 180;
const clockwise = config.clockwise !== false;
// Calculate attachment points on node circle
const startAngle = angle - Math.PI / 4;
const endAngle = angle + Math.PI / 4;
const start = {
x: nodePos.x + nodeRadius * Math.cos(startAngle),
y: nodePos.y + nodeRadius * Math.sin(startAngle)
};
const end = {
x: nodePos.x + nodeRadius * Math.cos(endAngle),
y: nodePos.y + nodeRadius * Math.sin(endAngle)
};
// Control points for bezier curve (creates loop outside node)
const controlOffset = loopRadius * (clockwise ? 1 : -1);
const controlAngle = angle;
const control1 = {
x: nodePos.x + (nodeRadius + controlOffset) * Math.cos(controlAngle - Math.PI / 3),
y: nodePos.y + (nodeRadius + controlOffset) * Math.sin(controlAngle - Math.PI / 3)
};
const control2 = {
x: nodePos.x + (nodeRadius + controlOffset) * Math.cos(controlAngle + Math.PI / 3),
y: nodePos.y + (nodeRadius + controlOffset) * Math.sin(controlAngle + Math.PI / 3)
};
return {
start,
control1,
control2,
end,
type: 'cubic-bezier'
};
}private renderSelfLoop(
ctx: CanvasRenderingContext2D,
edge: GraphEdge,
style: EdgeStyle
): void {
const node = this.getNodePosition(edge.source);
const path = calculateSelfLoopPath(node, 30, style.selfLoop || {});
// Draw cubic bezier curve
ctx.beginPath();
ctx.moveTo(path.start.x, path.start.y);
ctx.bezierCurveTo(
path.control1.x, path.control1.y,
path.control2.x, path.control2.y,
path.end.x, path.end.y
);
ctx.stroke();
// Draw arrow at end if configured
if (style.arrow?.position === 'forward' || style.arrow?.position === 'both') {
// Calculate tangent at end point for arrow direction
const t = 1.0;
const tangent = this.getBezierTangent(path, t);
this.drawArrowAtPoint(ctx, path.end, tangent, style.arrow);
}
}class EdgeBundler {
/**
* Calculate offsets for multiple edges between same nodes
*/
calculateParallelOffsets(edges: GraphEdge[]): Map<string, number> {
const offsets = new Map<string, number>();
// Group edges by source-target pair
const edgeGroups = new Map<string, GraphEdge[]>();
edges.forEach(edge => {
const key = `${edge.source}-${edge.target}`;
if (!edgeGroups.has(key)) {
edgeGroups.set(key, []);
}
edgeGroups.get(key)!.push(edge);
});
// Calculate offsets for each group
edgeGroups.forEach((group, key) => {
if (group.length === 1) {
// Single edge - no offset
offsets.set(group[0].id, 0);
} else {
// Multiple edges - distribute symmetrically
const spacing = 20; // pixels between parallel edges
const totalWidth = (group.length - 1) * spacing;
const startOffset = -totalWidth / 2;
group.forEach((edge, index) => {
offsets.set(edge.id, startOffset + index * spacing);
});
}
});
return offsets;
}
/**
* Calculate bezier curve for offset edge
*/
calculateOffsetBezier(
source: Position,
target: Position,
offset: number
): QuadraticBezierPath {
// Perpendicular offset direction
const dx = target.x - source.x;
const dy = target.y - source.y;
const len = Math.sqrt(dx * dx + dy * dy);
const perpX = -dy / len;
const perpY = dx / len;
// Control point offset perpendicular to edge
const midX = (source.x + target.x) / 2;
const midY = (source.y + target.y) / 2;
const controlX = midX + perpX * offset;
const controlY = midY + perpY * offset;
return {
start: source,
control: { x: controlX, y: controlY },
end: target,
type: 'quadratic-bezier'
};
}
}function renderBidirectionalEdge(
ctx: CanvasRenderingContext2D,
edge: GraphEdge,
style: EdgeStyle
): void {
const source = getNodePosition(edge.source);
const target = getNodePosition(edge.target);
// Option 1: Single edge with arrows on both ends
if (style.bidirectional === 'single') {
ctx.beginPath();
ctx.moveTo(source.x, source.y);
ctx.lineTo(target.x, target.y);
ctx.stroke();
// Arrows on both ends
drawArrow(ctx, source, target, style.arrow!);
drawArrow(ctx, target, source, style.arrow!);
}
// Option 2: Parallel edges with opposite directions
else if (style.bidirectional === 'parallel') {
const offset = 8; // pixels between parallel edges
// Forward edge (slightly offset)
const forwardPath = calculateOffsetBezier(source, target, offset / 2);
renderBezierPath(ctx, forwardPath);
drawArrowAtBezierEnd(ctx, forwardPath, style.arrow!);
// Backward edge (opposite offset)
const backwardPath = calculateOffsetBezier(target, source, offset / 2);
renderBezierPath(ctx, backwardPath);
drawArrowAtBezierEnd(ctx, backwardPath, style.arrow!);
}
}This is where EdgeCraft truly differentiates itself from competitors. We provide an elegant abstraction for complex RDF semantics.
interface RDFInverseEdge extends GraphEdge {
// Primary direction
predicate: string; // e.g., "hasPart"
// Inverse direction
inversePredicate?: string; // e.g., "partOf"
// Optional: OWL inverse property URI
inversePropertyURI?: string; // e.g., "http://example.org/partOf"
}
interface RDFEdgeStyle extends EdgeStyle {
// RDF-specific glyph configuration
showInverseGlyphs?: boolean;
// Glyph styling
glyphStyle?: {
size: number;
shape: 'circle' | 'diamond' | 'hexagon';
fill: string;
stroke: string;
textColor: string;
fontSize: number;
};
// Interaction
onGlyphClick?: (predicate: string, direction: 'forward' | 'backward') => void;
onGlyphHover?: (predicate: string, direction: 'forward' | 'backward') => void;
}class RDFEdgeRenderer {
/**
* Render edge with bidirectional RDF predicates
*/
renderRDFInverseEdge(
ctx: CanvasRenderingContext2D,
edge: RDFInverseEdge,
style: RDFEdgeStyle
): void {
const source = this.getNodePosition(edge.source);
const target = this.getNodePosition(edge.target);
// Draw main edge line
ctx.beginPath();
ctx.moveTo(source.x, source.y);
ctx.lineTo(target.x, target.y);
ctx.stroke();
if (!style.showInverseGlyphs || !edge.inversePredicate) {
// Simple edge with single arrow
this.drawArrow(ctx, source, target, style.arrow!);
return;
}
// Calculate midpoint
const midX = (source.x + target.x) / 2;
const midY = (source.y + target.y) / 2;
// Edge angle
const angle = Math.atan2(target.y - source.y, target.x - source.x);
// Perpendicular offset for glyph placement
const offset = 15; // pixels from edge
const perpX = -Math.sin(angle);
const perpY = Math.cos(angle);
// Forward predicate glyph (top/right side)
const forwardGlyphPos = {
x: midX + perpX * offset,
y: midY + perpY * offset
};
this.renderPredicate Glyph(
ctx,
forwardGlyphPos,
edge.predicate,
'forward',
angle,
style
);
// Inverse predicate glyph (bottom/left side)
const inverseGlyphPos = {
x: midX - perpX * offset,
y: midY - perpY * offset
};
this.renderPredicateGlyph(
ctx,
inverseGlyphPos,
edge.inversePredicate,
'backward',
angle + Math.PI, // Opposite direction
style
);
// Store glyph positions for interaction handling
this.glyphHitAreas.set(`${edge.id}-forward`, {
center: forwardGlyphPos,
radius: style.glyphStyle?.size || 12,
predicate: edge.predicate,
direction: 'forward'
});
this.glyphHitAreas.set(`${edge.id}-inverse`, {
center: inverseGlyphPos,
radius: style.glyphStyle?.size || 12,
predicate: edge.inversePredicate,
direction: 'backward'
});
}
/**
* Render individual predicate glyph with label
*/
private renderPredicateGlyph(
ctx: CanvasRenderingContext2D,
position: Position,
label: string,
direction: 'forward' | 'backward',
angle: number,
style: RDFEdgeStyle
): void {
const glyphSize = style.glyphStyle?.size || 12;
const shape = style.glyphStyle?.shape || 'diamond';
ctx.save();
ctx.translate(position.x, position.y);
// Draw glyph shape
ctx.fillStyle = style.glyphStyle?.fill || '#4a90e2';
ctx.strokeStyle = style.glyphStyle?.stroke || '#2c5aa0';
ctx.lineWidth = 2;
if (shape === 'diamond') {
ctx.beginPath();
ctx.moveTo(glyphSize, 0);
ctx.lineTo(0, glyphSize);
ctx.lineTo(-glyphSize, 0);
ctx.lineTo(0, -glyphSize);
ctx.closePath();
} else if (shape === 'circle') {
ctx.beginPath();
ctx.arc(0, 0, glyphSize, 0, Math.PI * 2);
} else if (shape === 'hexagon') {
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const hexAngle = (Math.PI / 3) * i - Math.PI / 2;
const x = glyphSize * Math.cos(hexAngle);
const y = glyphSize * Math.sin(hexAngle);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
}
ctx.fill();
ctx.stroke();
// Draw directional indicator (small arrow inside glyph)
const arrowSize = glyphSize * 0.4;
const arrowAngle = direction === 'forward' ? 0 : Math.PI;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-arrowSize, 0);
ctx.lineTo(arrowSize, 0);
ctx.moveTo(arrowSize * 0.5, -arrowSize * 0.5);
ctx.lineTo(arrowSize, 0);
ctx.lineTo(arrowSize * 0.5, arrowSize * 0.5);
ctx.stroke();
// Draw label next to glyph
ctx.restore();
// Position label perpendicular to edge
const labelOffset = glyphSize + 5;
const perpAngle = angle + Math.PI / 2;
const labelX = position.x + Math.cos(perpAngle) * labelOffset;
const labelY = position.y + Math.sin(perpAngle) * labelOffset;
ctx.font = `${style.glyphStyle?.fontSize || 10}px Arial`;
ctx.fillStyle = style.glyphStyle?.textColor || '#333';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.formatPredicateLabel(label), labelX, labelY);
}
/**
* Format predicate label (abbreviate if needed)
*/
private formatPredicateLabel(predicate: string): string {
// Remove namespace prefix if present
const parts = predicate.split(/[:#]/);
const label = parts[parts.length - 1];
// CamelCase to space-separated
return label.replace(/([A-Z])/g, ' $1').trim();
}
/**
* Handle clicks on glyphs
*/
handleGlyphClick(position: Position): void {
for (const [key, hitArea] of this.glyphHitAreas) {
const dist = Math.sqrt(
Math.pow(position.x - hitArea.center.x, 2) +
Math.pow(position.y - hitArea.center.y, 2)
);
if (dist <= hitArea.radius) {
// Glyph clicked
if (this.style.onGlyphClick) {
this.style.onGlyphClick(hitArea.predicate, hitArea.direction);
}
// Show tooltip or modal with predicate details
this.showPredicateDetails(hitArea.predicate, hitArea.direction);
break;
}
}
}
}const graph = new EdgeCraft({
container: '#graph',
edgeStyle: {
arrow: {
position: 'forward', // Arrow points from source to target
size: 12,
shape: 'triangle',
filled: true
}
}
});graph.addEdge({
id: 'self-loop-1',
source: 'node-1',
target: 'node-1', // Same as source
label: 'self-reference'
});
// Style self-loops
graph.setEdgeStyle('self-loop-1', {
selfLoop: {
radius: 50,
angle: 45, // Position at 45° from node
clockwise: true
},
arrow: {
position: 'forward'
}
});// Add multiple relationships
graph.addEdge({ id: 'e1', source: 'A', target: 'B', label: 'knows' });
graph.addEdge({ id: 'e2', source: 'A', target: 'B', label: 'worksAt' });
graph.addEdge({ id: 'e3', source: 'A', target: 'B', label: 'manages' });
// Automatically bundles as parallel bezier curves
// Or manually configure:
graph.setEdgeStyle('e1', { parallelOffset: -20 });
graph.setEdgeStyle('e2', { parallelOffset: 0 });
graph.setEdgeStyle('e3', { parallelOffset: 20 });graph.addEdge({
id: 'bidirectional-1',
source: 'A',
target: 'B',
label: 'friend'
});
graph.setEdgeStyle('bidirectional-1', {
arrow: {
position: 'both', // Arrows on both ends
size: 10
},
bidirectional: 'single' // Single edge with two arrows
});// Create RDF graph with inverse properties
const rdfGraph = new EdgeCraft({
container: '#rdf-graph',
edgeStyle: (edge) => {
if (edge.inversePredicate) {
return {
showInverseGlyphs: true,
glyphStyle: {
size: 14,
shape: 'diamond',
fill: '#667eea',
stroke: '#5a67d8',
textColor: '#2d3748',
fontSize: 11
},
onGlyphClick: (predicate, direction) => {
console.log(`Clicked ${direction} predicate: ${predicate}`);
// Open SPARQL query editor, show predicate details, etc.
}
};
}
return { arrow: { position: 'forward' } };
}
});
// Add inverse relationship
rdfGraph.addEdge({
id: 'hasPart-rel',
source: 'Car123',
target: 'Engine456',
predicate: 'hasPart',
inversePredicate: 'partOf',
inversePropertyURI: 'http://example.org/ontology#partOf'
});
// The edge will render with two interactive glyphs:
// - Top/right: "has Part" pointing forward →
// - Bottom/left: "part Of" pointing backward ←// Model an OWL association class with inverse relationships
rdfGraph.addEdge({
id: 'employment-rel',
source: 'Person:Alice',
target: 'Company:Acme',
predicate: 'employedBy',
inversePredicate: 'employs',
associationClass: {
id: 'Employment:123',
properties: {
startDate: '2023-01-15',
position: 'Senior Engineer',
salary: 120000
}
}
});
// Renders as:
// [Person:Alice] ←employs— [Employment:123] —employedBy→ [Company:Acme]
// With interactive glyphs on each segment| Feature | Cytoscape.js | EdgeCraft |
|---|---|---|
| Basic arrows | ✅ | ✅ |
| Self-loops | ✅ | ✅ |
| Multi-edges | ✅ (automatic bundling) | |
| Bidirectional | ✅ (single edge with dual arrows) | |
| RDF inverse glyphs | ❌ | ✅ Unique feature |
| Interactive predicates | ❌ | ✅ Unique feature |
| Feature | Keylines | EdgeCraft |
|---|---|---|
| Basic arrows | ✅ | ✅ |
| Self-loops | ✅ | ✅ |
| Multi-edges | ✅ | ✅ |
| Bidirectional | ✅ | ✅ |
| RDF inverse glyphs | ❌ | ✅ Unique feature |
| Open source | ❌ (£5k+/year) | ✅ Free |
- Arrow rendering in Canvas renderer
- Arrow rendering in WebGL renderer
- Self-loop geometry calculations
- Self-loop rendering
- Basic arrow configuration API
- Edge bundler service
- Parallel edge offset calculation
- Automatic multi-edge detection
- Bezier curve rendering for offset edges
- Bidirectional edge detection
- Dual-arrow rendering
- Parallel bidirectional edges
- API for bidirectional configuration
- Glyph rendering system
- Interactive glyph hit detection
- Predicate label formatting
- RDF edge data model
- Inverse property normalization
- Glyph click/hover handlers
- Tooltip system for predicates
- Association class visualization
- OWL property chains
- Subsetting/redefining relationships
- Facet/facet-of patterns
- SPARQL query integration
describe('Arrow Rendering', () => {
it('should render forward arrow', () => {
const edge = { source: 'A', target: 'B' };
const style = { arrow: { position: 'forward' } };
const result = renderEdge(edge, style);
expect(result.hasArrow).toBe(true);
expect(result.arrowDirection).toBe('forward');
});
});
describe('Self-Loop Geometry', () => {
it('should calculate self-loop bezier path', () => {
const path = calculateSelfLoopPath({ x: 100, y: 100 }, 30, {});
expect(path.type).toBe('cubic-bezier');
expect(path.start).not.toEqual(path.end);
});
});
describe('RDF Inverse Glyphs', () => {
it('should render glyphs for inverse predicates', () => {
const edge = {
source: 'A',
target: 'B',
predicate: 'hasPart',
inversePredicate: 'partOf'
};
const glyphs = renderRDFGlyphs(edge);
expect(glyphs).toHaveLength(2);
expect(glyphs[0].direction).toBe('forward');
expect(glyphs[1].direction).toBe('backward');
});
});- Arrow shapes at various angles
- Self-loop curves at different positions
- Multi-edge bundling patterns
- RDF glyph rendering
- Interactive glyph hover states
- 10,000 edges with arrows (target: 60fps)
- 1,000 self-loops
- 500 multi-edge bundles
-
Edge Styling Guide
- Arrow configuration examples
- Self-loop patterns
- Multi-edge handling
-
RDF Visualization Guide (New!)
- Inverse property modeling
- Association classes
- OWL patterns
- SPARQL integration
-
API Reference
ArrowConfiginterfaceSelfLoopConfiginterfaceEdgeGlyphConfiginterfaceRDFEdgeStyleinterface
-
Interactive Examples
- Directed graph demo
- Self-referential graph
- Multi-relationship visualization
- RDF ontology explorer (showcase)
These advanced edge rendering features position EdgeCraft as the premier choice for:
- Knowledge graph visualization - RDF inverse glyphs are game-changing
- Ontology exploration - OWL patterns made visual
- Complex network analysis - Multi-edges, self-loops handled elegantly
- Semantic web applications - First-class RDF support
The RDF inverse relationship glyphs are EdgeCraft's killer feature - no other open-source (or even commercial) library provides this level of sophistication for RDF visualization. This alone will attract the semantic web community and position EdgeCraft as the standard tool for ontology visualization.
Estimated impact on competitive position:
- Current: 6.5/10
- After implementation: 8.5/10 (leap-frog competitors in RDF/ontology space)
- Market differentiation: High (unique features, open source advantage)