Skip to content

Commit 5d0d626

Browse files
Prepared for first full release.
1 parent 3f24916 commit 5d0d626

File tree

5 files changed

+234
-29
lines changed

5 files changed

+234
-29
lines changed

src/KristofferStrube.Blazor.GraphEditor/Edge.cs

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,67 @@
44

55
namespace KristofferStrube.Blazor.GraphEditor;
66

7+
/// <summary>
8+
/// The edge between two <see cref="Node{TNodeData, TEdgeData}"/>s.
9+
/// </summary>
10+
/// <typeparam name="TNodeData">The type parameter for the data that backs the nodes in this graph.</typeparam>
11+
/// <typeparam name="TEdgeData">The type parameter for the data that backs the nodes in this graph.</typeparam>
712
public class Edge<TNodeData, TEdgeData> : Line where TNodeData : IEquatable<TNodeData>
813
{
14+
/// <summary>
15+
/// Constructs an edge.
16+
/// </summary>
17+
/// <param name="element">The SVG element that will be used as the base backing element for changes to the position of the edge.</param>
18+
/// <param name="svg">The <see cref="SVGEditor.SVGEditor"/> that the edge resides in.</param>
919
public Edge(IElement element, SVGEditor.SVGEditor svg) : base(element, svg)
1020
{
1121
UpdateLine();
1222
}
1323

24+
/// <summary>
25+
/// The component type that will be used to edit this shape.
26+
/// </summary>
1427
public override Type Presenter => typeof(EdgeEditor<TNodeData, TEdgeData>);
1528

16-
public GraphEditor<TNodeData, TEdgeData> GraphEditor { get; set; }
17-
18-
public TEdgeData Data { get; set; }
19-
20-
public Node<TNodeData, TEdgeData> From { get; set; }
21-
22-
public Node<TNodeData, TEdgeData> To { get; set; }
23-
29+
/// <summary>
30+
/// The <see cref="GraphEditor{TNode, TEdge}"/> that this edge belongs to.
31+
/// </summary>
32+
public required GraphEditor<TNodeData, TEdgeData> GraphEditor { get; set; }
33+
34+
/// <summary>
35+
/// The data that this edge will get its characteristics from.
36+
/// </summary>
37+
public required TEdgeData Data { get; set; }
38+
39+
/// <summary>
40+
/// The node that the edge goes from.
41+
/// </summary>
42+
public required Node<TNodeData, TEdgeData> From { get; set; }
43+
44+
/// <summary>
45+
/// The node that the edge goes to.
46+
/// </summary>
47+
public required Node<TNodeData, TEdgeData> To { get; set; }
48+
49+
/// <summary>
50+
/// The width of the edge mapped from its <see cref="Data"/>.
51+
/// </summary>
2452
public new string StrokeWidth => GraphEditor.EdgeWidthMapper(Data).AsString();
2553

54+
/// <summary>
55+
/// The color of the edge mapped from its <see cref="Data"/>.
56+
/// </summary>
2657
public new string Stroke => GraphEditor.EdgeColorMapper(Data);
2758

59+
/// <summary>
60+
/// Whether the edge should show an arrow head at its <see cref="To"/> end.
61+
/// </summary>
2862
public bool ShowArrow => GraphEditor.EdgeShowsArrow(Data);
2963

64+
/// <summary>
65+
/// Handles when the shape is moved.
66+
/// </summary>
67+
/// <param name="eventArgs">The arguments from the pointer that is moved.</param>
3068
public override void HandlePointerMove(PointerEventArgs eventArgs)
3169
{
3270
if (SVG.EditMode is EditMode.Add)
@@ -36,6 +74,10 @@ public override void HandlePointerMove(PointerEventArgs eventArgs)
3674
}
3775
}
3876

77+
/// <summary>
78+
/// Handles when the shape stops moving.
79+
/// </summary>
80+
/// <param name="eventArgs">The arguments from the pointer that stops.</param>
3981
public override void HandlePointerUp(PointerEventArgs eventArgs)
4082
{
4183
if (SVG.EditMode is EditMode.Add
@@ -54,6 +96,9 @@ public override void HandlePointerUp(PointerEventArgs eventArgs)
5496
}
5597
}
5698

99+
/// <summary>
100+
/// The method that is invoked when an edge finishes its creation process.
101+
/// </summary>
57102
public override void Complete()
58103
{
59104
if (To is null)
@@ -63,6 +108,10 @@ public override void Complete()
63108
}
64109
}
65110

111+
/// <summary>
112+
/// Updates the coordinates of the start of the line.
113+
/// </summary>
114+
/// <param name="towards">The coordinates of where the edge will go towards.</param>
66115
public void SetStart((double x, double y) towards)
67116
{
68117
double differenceX = towards.x - From!.Cx;
@@ -76,6 +125,9 @@ public void SetStart((double x, double y) towards)
76125
}
77126
}
78127

128+
/// <summary>
129+
/// Updates the coordinates of the start and end of the edge.
130+
/// </summary>
79131
public void UpdateLine()
80132
{
81133
if (From is null || To is null)
@@ -100,6 +152,15 @@ public void UpdateLine()
100152
}
101153
}
102154

155+
/// <summary>
156+
/// Adds a new edge.
157+
/// </summary>
158+
/// <param name="SVG">The <see cref="SVGEditor.SVGEditor"/> that the shape will be create in.</param>
159+
/// <param name="graphEditor">The <see cref="GraphEditor{TNode, TEdge}"/> that the edge resides in.</param>
160+
/// <param name="data">The backing data that the edge will be created from.</param>
161+
/// <param name="from">The <see cref="Node{TNodeData, TEdgeData}"/> that the edge goes from.</param>
162+
/// <param name="to">The <see cref="Node{TNodeData, TEdgeData}"/> that the edge goes to.</param>
163+
/// <returns>The new edge.</returns>
103164
public static Edge<TNodeData, TEdgeData> AddNew(
104165
SVGEditor.SVGEditor SVG,
105166
GraphEditor<TNodeData, TEdgeData> graphEditor,

src/KristofferStrube.Blazor.GraphEditor/GraphEditor.razor.cs

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
namespace 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>
611
public 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"),
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
namespace KristofferStrube.Blazor.GraphEditor;
22

3+
/// <summary>
4+
/// The context that specifies any functions that needs to be triggered for specific events in the graph..
5+
/// </summary>
36
public class GraphEditorCallbackContext
47
{
8+
/// <summary>
9+
/// The function that will be invoked when a node is selected by getting focus.
10+
/// </summary>
511
public required Func<string, Task> NodeSelectionCallback { get; set; }
612
}

src/KristofferStrube.Blazor.GraphEditor/KristofferStrube.Blazor.GraphEditor.csproj

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<TargetFramework>net7.0</TargetFramework>
4+
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
55
<ImplicitUsings>true</ImplicitUsings>
66
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
77
<LangVersion>preview</LangVersion>
@@ -17,6 +17,7 @@
1717
<Authors>Kristoffer Strube</Authors>
1818
<PackageReadmeFile>README.md</PackageReadmeFile>
1919
<PackageIcon>icon.png</PackageIcon>
20+
<GenerateDocumentationFile>True</GenerateDocumentationFile>
2021
</PropertyGroup>
2122

2223
<ItemGroup>
@@ -36,7 +37,14 @@
3637

3738
<ItemGroup>
3839
<PackageReference Include="KristofferStrube.Blazor.SVGEditor" Version="0.3.0" />
39-
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.8" />
40+
</ItemGroup>
41+
42+
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
43+
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.20" />
44+
</ItemGroup>
45+
46+
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
47+
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.10" />
4048
</ItemGroup>
4149

4250
</Project>

0 commit comments

Comments
 (0)