33
44namespace KristofferStrube . Blazor . GraphEditor ;
55
6+ /// <summary>
7+ /// An editor for graphs consisting of nodes and edges.
8+ /// </summary>
9+ /// <typeparam name="TNode">The type that will represent the nodes in graph.</typeparam>
10+ /// <typeparam name="TEdge">The type that will represent the connections between the nodes in the graph.</typeparam>
611public partial class GraphEditor < TNode , TEdge > : ComponentBase where TNode : IEquatable < TNode >
712{
813 private GraphEditorCallbackContext callbackContext = default ! ;
@@ -12,6 +17,9 @@ private string EdgeId(TEdge e)
1217 return NodeIdMapper ( EdgeFromMapper ( e ) ) + "-" + NodeIdMapper ( EdgeToMapper ( e ) ) ;
1318 }
1419
20+ /// <summary>
21+ /// Maps each node to an unique id.
22+ /// </summary>
1523 [ Parameter , EditorRequired ]
1624 public required Func < TNode , string > NodeIdMapper { get ; set ; }
1725
@@ -39,9 +47,15 @@ private string EdgeId(TEdge e)
3947 [ Parameter ]
4048 public Func < TNode , string ? > NodeImageMapper { get ; set ; } = _ => null ;
4149
50+ /// <summary>
51+ /// Maps each edge to which node it goes from.
52+ /// </summary>
4253 [ Parameter , EditorRequired ]
4354 public required Func < TEdge , TNode > EdgeFromMapper { get ; set ; }
4455
56+ /// <summary>
57+ /// Maps each edge to which node it goes to.
58+ /// </summary>
4559 [ Parameter , EditorRequired ]
4660 public required Func < TEdge , TNode > EdgeToMapper { get ; set ; }
4761
@@ -69,25 +83,44 @@ private string EdgeId(TEdge e)
6983 [ Parameter ]
7084 public Func < TEdge , string > EdgeColorMapper { get ; set ; } = _ => "#000000" ;
7185
72- [ Parameter ]
7386 /// <summary>
7487 /// Defaults to <see langword="true"/>
7588 /// </summary>
89+ [ Parameter ]
7690 public Func < TEdge , bool > EdgeShowsArrow { get ; set ; } = _ => true ;
7791
92+ /// <summary>
93+ /// Callback that will be invoked when a specific node is selected by it getting focus.
94+ /// </summary>
7895 [ Parameter ]
7996 public Func < TNode , Task > ? NodeSelectionCallback { get ; set ; }
8097
98+ /// <summary>
99+ /// Whether the underlying <see cref="SVGEditor.SVGEditor"/> has rendered.
100+ /// </summary>
81101 public bool IsReadyToLoad => SVGEditor . BBox is not null ;
82102
103+ /// <summary>
104+ /// The nodes of the graph.
105+ /// </summary>
83106 protected Dictionary < string , TNode > Nodes { get ; set ; } = [ ] ;
84107
108+ /// <summary>
109+ /// The edges of the graph.
110+ /// </summary>
85111 protected Dictionary < string , TEdge > Edges { get ; set ; } = [ ] ;
86112
113+ /// <summary>
114+ /// The underlying <see cref="SVGEditor.SVGEditor"/>.
115+ /// </summary>
87116 public SVGEditor . SVGEditor SVGEditor { get ; set ; } = default ! ;
88117
118+ /// <summary>
119+ /// A text representation of the graph.
120+ /// </summary>
89121 protected string Input { get ; set ; } = "" ;
90122
123+ /// <inheritdoc/>
91124 protected override void OnInitialized ( )
92125 {
93126 callbackContext = new ( )
@@ -102,6 +135,11 @@ protected override void OnInitialized()
102135 } ;
103136 }
104137
138+ /// <summary>
139+ /// Loads a graph of nodes and edges.
140+ /// </summary>
141+ /// <param name="nodes">The nodes of the graph.</param>
142+ /// <param name="edges">The edges that are present between the given <paramref name="nodes"/>.</param>
105143 public async Task LoadGraph ( List < TNode > nodes , List < TEdge > edges )
106144 {
107145 if ( SVGEditor . BBox is not null )
@@ -139,7 +177,11 @@ public async Task LoadGraph(List<TNode> nodes, List<TEdge> edges)
139177 nodeElements = SVGEditor . Elements . Where ( e => e is Node < TNode , TEdge > ) . Select ( e => ( Node < TNode , TEdge > ) e ) . ToArray ( ) ;
140178 }
141179
142-
180+ /// <summary>
181+ /// Updates the nodes and edges that are in the graph without clearing the existing ones.
182+ /// </summary>
183+ /// <param name="nodes">The nodes of the graph.</param>
184+ /// <param name="edges">The edges that are present between the given <paramref name="nodes"/>.</param>
143185 public async Task UpdateGraph ( List < TNode > nodes , List < TEdge > edges )
144186 {
145187 Dictionary < TNode , Node < TNode , TEdge > > newNodeElementDictionary = [ ] ;
@@ -151,11 +193,10 @@ public async Task UpdateGraph(List<TNode> nodes, List<TEdge> edges)
151193 foreach ( TNode node in nodes )
152194 {
153195 string nodeKey = NodeIdMapper ( node ) ;
154- if ( ! Nodes . ContainsKey ( nodeKey ) )
196+ if ( Nodes . TryAdd ( nodeKey , node ) )
155197 {
156198 Node < TNode , TEdge > element = Node < TNode , TEdge > . CreateNew ( SVGEditor , this , node ) ;
157199 newNodeElementDictionary . Add ( node , element ) ;
158- Nodes . Add ( nodeKey , node ) ;
159200 }
160201 newSetOfNodes . Add ( nodeKey ) ;
161202 }
@@ -164,14 +205,13 @@ public async Task UpdateGraph(List<TNode> nodes, List<TEdge> edges)
164205 foreach ( TEdge edge in edges )
165206 {
166207 string edgeKey = EdgeId ( edge ) ;
167- if ( ! Edges . ContainsKey ( edgeKey ) )
208+ if ( Edges . TryAdd ( edgeKey , edge ) )
168209 {
169210 TNode from = EdgeFromMapper ( edge ) ;
170211 TNode to = EdgeToMapper ( edge ) ;
171212 Node < TNode , TEdge > fromElement = newNodeElementDictionary . TryGetValue ( from , out var eFrom ) ? eFrom : nodeElements . First ( n => n . Data . Equals ( from ) ) ;
172213 Node < TNode , TEdge > toElement = newNodeElementDictionary . TryGetValue ( to , out var eTo ) ? eTo : nodeElements . First ( n => n . Data . Equals ( to ) ) ;
173214 Edge < TNode , TEdge > . AddNew ( SVGEditor , this , edge , fromElement , toElement ) ;
174- Edges . Add ( edgeKey , edge ) ;
175215 }
176216 newSetOfEdges . Add ( edgeKey ) ;
177217 }
@@ -211,15 +251,18 @@ public async Task UpdateGraph(List<TNode> nodes, List<TEdge> edges)
211251 double newY = 0 ;
212252 foreach ( var neighborEdge in newNodeElement . Edges )
213253 {
214- var neighborMirroredPosition = MirroredAverageOfNeighborNodes ( neighborEdge , newNodeElement ) ;
215- newX += neighborMirroredPosition . x / newNodeElement . Edges . Count ( ) ;
216- newY += neighborMirroredPosition . y / newNodeElement . Edges . Count ( ) ;
254+ var ( x , y ) = MirroredAverageOfNeighborNodes ( neighborEdge , newNodeElement ) ;
255+ newX += x / newNodeElement . Edges . Count ;
256+ newY += y / newNodeElement . Edges . Count ;
217257 }
218258 newNodeElement . Cx = newX ;
219259 newNodeElement . Cy = newY ;
220260 }
221261 SVGEditor . AddElement ( newNodeElement ) ;
222262 }
263+
264+ await Task . Yield ( ) ;
265+ StateHasChanged ( ) ;
223266 nodeElements = SVGEditor . Elements . Where ( e => e is Node < TNode , TEdge > ) . Select ( e => ( Node < TNode , TEdge > ) e ) . ToArray ( ) ;
224267
225268 foreach ( Edge < TNode , TEdge > edge in SVGEditor . Elements . Where ( e => e is Edge < TNode , TEdge > ) . Cast < Edge < TNode , TEdge > > ( ) )
@@ -259,18 +302,21 @@ public async Task UpdateGraph(List<TNode> nodes, List<TEdge> edges)
259302 y : averageYPositionOfNeighborsNeighbors - neighborNode . Cy
260303 ) ;
261304 var distanceBetweenAverageNeighborsNeighborsAndNeighbor = Math . Sqrt ( Math . Pow ( differenceBetweenAverageNeighborsNeighborsAndNeighbor . x , 2 ) + Math . Pow ( differenceBetweenAverageNeighborsNeighborsAndNeighbor . y , 2 ) ) ;
262- var normalizedVectorBetweenAverageNeighborsNeighborsAndNeighbor = (
263- x : differenceBetweenAverageNeighborsNeighborsAndNeighbor . x / distanceBetweenAverageNeighborsNeighborsAndNeighbor ,
264- y : differenceBetweenAverageNeighborsNeighborsAndNeighbor . y / distanceBetweenAverageNeighborsNeighborsAndNeighbor
305+ var ( x , y ) = (
306+ differenceBetweenAverageNeighborsNeighborsAndNeighbor . x / distanceBetweenAverageNeighborsNeighborsAndNeighbor ,
307+ differenceBetweenAverageNeighborsNeighborsAndNeighbor . y / distanceBetweenAverageNeighborsNeighborsAndNeighbor
265308 ) ;
266309
267310 return (
268- x : neighborNode . Cx - normalizedVectorBetweenAverageNeighborsNeighborsAndNeighbor . x * edgeLength ,
269- y : neighborNode . Cy - normalizedVectorBetweenAverageNeighborsNeighborsAndNeighbor . y * edgeLength
311+ x : neighborNode . Cx - x * edgeLength ,
312+ y : neighborNode . Cy - y * edgeLength
270313 ) ;
271314 }
272315 }
273316
317+ /// <summary>
318+ /// Updates the layout of the nodes so that they repulse each other while staying close to the ones that they are connected to via edges.
319+ /// </summary>
274320 public Task ForceDirectedLayout ( )
275321 {
276322 Span < Node < TNode , TEdge > > nodes = nodeElements . AsSpan ( ) ;
@@ -329,18 +375,24 @@ public Task ForceDirectedLayout()
329375 return Task . CompletedTask ;
330376 }
331377
378+ /// <summary>
379+ /// Move all edges to the back so that they are hidden if any nodes are displayed in the same position.
380+ /// </summary>
332381 public void MoveEdgesToBack ( )
333382 {
334383 var prevSelectedShapes = SVGEditor . SelectedShapes . ToList ( ) ;
335384 SVGEditor . ClearSelectedShapes ( ) ;
336- foreach ( Shape shape in SVGEditor . Elements . Where ( e => e is Edge < TNode , TEdge > ) )
385+ foreach ( Shape shape in SVGEditor . Elements . Where ( e => e is Edge < TNode , TEdge > ) . Cast < Shape > ( ) )
337386 {
338387 SVGEditor . SelectShape ( shape ) ;
339388 }
340389 SVGEditor . MoveToBack ( ) ;
341390 SVGEditor . SelectedShapes = prevSelectedShapes ;
342391 }
343392
393+ /// <summary>
394+ /// The elements that can be rendered in this graph. This normally doesn't need adjustments as the graph defaults to having support for the nodes and edges it shows.
395+ /// </summary>
344396 protected List < SupportedElement > SupportedElements { get ; set ; } =
345397 [
346398 new ( typeof ( Node < TNode , TEdge > ) , element => element . TagName is "CIRCLE" && element . GetAttribute ( "data-elementtype" ) == "node" ) ,
0 commit comments