Skip to content

Commit

Permalink
Added support for auto-placement of new nodes with multiple neighbors.
Browse files Browse the repository at this point in the history
  • Loading branch information
KristofferStrube committed Nov 25, 2023
1 parent 4374003 commit 8ce27ef
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
p.Radians = p.BaseOffset + unixTimeSeconds % (Math.PI * 2);
});
await GraphEditor.ForceDirectedLayout();
GraphEditor.FitToShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 2, 1));
GraphEditor.FitToShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 2, 1), 50);
prevUnixTimeSeconds = unixTimeSeconds;
await Task.Delay(1);
}
Expand All @@ -76,6 +76,14 @@
{
pages.Add(new Page(nodeCount++.ToString()) { BaseOffset = DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1000.0 });
transitions.Add(new Transition(pages[Random.Shared.Next(nodeCount - 2)], pages[^1], 1, 200));
if (Random.Shared.NextDouble() < 0.07)
{
transitions.Add(new Transition(pages[Random.Shared.Next(nodeCount - 2)], pages[^1], 1, 200));
}
if (Random.Shared.NextDouble() < 0.07)
{
transitions.Add(new Transition(pages[Random.Shared.Next(nodeCount - 2)], pages[^1], 1, 200));
}
await GraphEditor.UpdateGraph(pages, transitions);
}

Expand Down
104 changes: 60 additions & 44 deletions src/KristofferStrube.Blazor.GraphEditor/GraphEditor.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private string EdgeId(TEdge e)
[Parameter]
public Func<TNode, Task>? NodeSelectionCallback { get; set; }

public bool IsReadyToLoad => !(SVGEditor.BBox is null);
public bool IsReadyToLoad => SVGEditor.BBox is not null;

protected override void OnInitialized()
{
Expand Down Expand Up @@ -132,55 +132,71 @@ public async Task UpdateGraph(List<TNode> nodes, List<TEdge> edges)
newNodeElement.Cx = Random.Shared.NextDouble() * 20;
newNodeElement.Cy = Random.Shared.NextDouble() * 20;
}
else // TODO: We should handle all edges not just the first
else
{
var singleEdge = newNodeElement.Edges.First();
var neighborNode = singleEdge.From == newNodeElement ? singleEdge.To : singleEdge.From;
var neighborsNeighbors = neighborNode.Edges.Select(e => e.From == neighborNode ? e.To : e.From).Where(n => n != newNodeElement).ToArray();

var edgeLength = EdgeSpringLengthMapper(singleEdge.Data);

if (neighborsNeighbors.Length is 0)
{
var randomAngle = neighborNode.Cx + Random.Shared.NextDouble() * Math.PI;
newNodeElement.Cx = neighborNode.Cx + Math.Sin(randomAngle) * edgeLength;
newNodeElement.Cy = neighborNode.Cy + Math.Cos(randomAngle) * edgeLength;
}
else
double newX = 0;
double newY = 0;
foreach (var neighborEdge in newNodeElement.Edges)
{
double averageXPositionOfNeighborsNeighbors = 0;
double averageYPositionOfNeighborsNeighbors = 0;

foreach (var neighborsNeighborNode in neighborsNeighbors)
{
averageXPositionOfNeighborsNeighbors += neighborsNeighborNode.Cx / neighborsNeighbors.Length;
averageYPositionOfNeighborsNeighbors += neighborsNeighborNode.Cy / neighborsNeighbors.Length;
}
// TODO: Handle the case where averagePositionOfNeighborsNeighbors==neighborsPosition;
var differenceBetweenAverageNeighborsNeighborsAndNeighbor = (
x: averageXPositionOfNeighborsNeighbors - neighborNode.Cx,
y: averageYPositionOfNeighborsNeighbors - neighborNode.Cy
);
var distanceBetweenAverageNeighborsNeighborsAndNeighbor = Math.Sqrt(Math.Pow(differenceBetweenAverageNeighborsNeighborsAndNeighbor.x, 2) + Math.Pow(differenceBetweenAverageNeighborsNeighborsAndNeighbor.y, 2));
var normalizedVectorBetweenAverageNeighborsNeighborsAndNeighbor = (
x: differenceBetweenAverageNeighborsNeighborsAndNeighbor.x / distanceBetweenAverageNeighborsNeighborsAndNeighbor,
y: differenceBetweenAverageNeighborsNeighborsAndNeighbor.y / distanceBetweenAverageNeighborsNeighborsAndNeighbor
);

newNodeElement.Cx = neighborNode.Cx - normalizedVectorBetweenAverageNeighborsNeighborsAndNeighbor.x * edgeLength;
newNodeElement.Cy = neighborNode.Cy - normalizedVectorBetweenAverageNeighborsNeighborsAndNeighbor.y * edgeLength;
var neighborMirroredPosition = MirroredAverageOfNeighborNodes(neighborEdge, newNodeElement);
newX += neighborMirroredPosition.x / newNodeElement.Edges.Count();
newY += neighborMirroredPosition.y / newNodeElement.Edges.Count();
}
newNodeElement.Cx = newX;
newNodeElement.Cy = newY;
}
SVGEditor.AddElement(newNodeElement);
}
nodeElements = SVGEditor.Elements.Where(e => e is Node<TNode, TEdge>).Select(e => (Node<TNode, TEdge>)e).ToArray();

foreach (Edge<TNode, TEdge> edge in SVGEditor.Elements.Where(e => e is Edge<TNode, TEdge>))
foreach (Edge<TNode, TEdge> edge in SVGEditor.Elements.Where(e => e is Edge<TNode, TEdge>).Cast<Edge<TNode, TEdge>>())
{
edge.UpdateLine();
}
}

private (double x, double y) MirroredAverageOfNeighborNodes(Edge<TNode, TEdge> neighborEdge, Node<TNode, TEdge> rootNode)
{
var neighborNode = neighborEdge.From == rootNode ? neighborEdge.To : neighborEdge.From;
var neighborsNeighbors = neighborNode.Edges.Select(e => e.From == neighborNode ? e.To : e.From).Where(n => n != rootNode).ToArray();

var edgeLength = EdgeSpringLengthMapper(neighborEdge.Data);

if (neighborsNeighbors.Length is 0)
{
var randomAngle = neighborNode.Cx + Random.Shared.NextDouble() * Math.PI;
return (
x: neighborNode.Cx + Math.Sin(randomAngle) * edgeLength,
y: neighborNode.Cy + Math.Cos(randomAngle) * edgeLength
);
}
else
{
double averageXPositionOfNeighborsNeighbors = 0;
double averageYPositionOfNeighborsNeighbors = 0;

foreach (var neighborsNeighborNode in neighborsNeighbors)
{
averageXPositionOfNeighborsNeighbors += neighborsNeighborNode.Cx / neighborsNeighbors.Length;
averageYPositionOfNeighborsNeighbors += neighborsNeighborNode.Cy / neighborsNeighbors.Length;
}
// TODO: Handle the case where averagePositionOfNeighborsNeighbors==neighborsPosition;
var differenceBetweenAverageNeighborsNeighborsAndNeighbor = (
x: averageXPositionOfNeighborsNeighbors - neighborNode.Cx,
y: averageYPositionOfNeighborsNeighbors - neighborNode.Cy
);
var distanceBetweenAverageNeighborsNeighborsAndNeighbor = Math.Sqrt(Math.Pow(differenceBetweenAverageNeighborsNeighborsAndNeighbor.x, 2) + Math.Pow(differenceBetweenAverageNeighborsNeighborsAndNeighbor.y, 2));
var normalizedVectorBetweenAverageNeighborsNeighborsAndNeighbor = (
x: differenceBetweenAverageNeighborsNeighborsAndNeighbor.x / distanceBetweenAverageNeighborsNeighborsAndNeighbor,
y: differenceBetweenAverageNeighborsNeighborsAndNeighbor.y / distanceBetweenAverageNeighborsNeighborsAndNeighbor
);

return (
x: neighborNode.Cx - normalizedVectorBetweenAverageNeighborsNeighborsAndNeighbor.x * edgeLength,
y: neighborNode.Cy - normalizedVectorBetweenAverageNeighborsNeighborsAndNeighbor.y * edgeLength
);
}
}

public Task ForceDirectedLayout()
{
Expand Down Expand Up @@ -221,7 +237,7 @@ public Task ForceDirectedLayout()
}
}

foreach (Edge<TNode, TEdge> edge in SVGEditor.Elements.Where(e => e is Edge<TNode, TEdge>))
foreach (Edge<TNode, TEdge> edge in SVGEditor.Elements.Where(e => e is Edge<TNode, TEdge>).Cast<Edge<TNode, TEdge>>())
{
edge.UpdateLine();
}
Expand All @@ -233,7 +249,7 @@ public void FitToShapes(double delta = 1, double padding = 20)
if (SVGEditor.BBox is null || SVGEditor.SelectedShapes.Count > 0) return;
double lowerX = double.MaxValue, lowerY = double.MaxValue;
double upperX = double.MinValue, upperY = double.MinValue;
foreach (Shape shape in SVGEditor.Elements)
foreach (Shape shape in SVGEditor.Elements.Cast<Shape>())
{
foreach ((double x, double y) in shape.SelectionPoints)
{
Expand All @@ -251,17 +267,17 @@ public void FitToShapes(double delta = 1, double padding = 20)
SVGEditor.Scale = (SVGEditor.Scale * (1 - delta) + newScale * delta);
SVGEditor.Translate = (SVGEditor.Translate.x * (1 - delta) + newTranslate.x * delta, SVGEditor.Translate.y * (1 - delta) + newTranslate.y * delta);
}
protected Dictionary<string, TNode> Nodes { get; set; } = new();
protected Dictionary<string, TNode> Nodes { get; set; } = [];

protected Dictionary<string, TEdge> Edges { get; set; } = new();
protected Dictionary<string, TEdge> Edges { get; set; } = [];

protected SVGEditor.SVGEditor SVGEditor { get; set; } = default!;

protected string Input { get; set; } = "";

protected List<SupportedElement> SupportedElements { get; set; } = new()
{
protected List<SupportedElement> SupportedElements { get; set; } =
[
new(typeof(Node<TNode, TEdge>), element => element.TagName is "CIRCLE" && element.GetAttribute("data-elementtype") == "node"),
new(typeof(Edge<TNode, TEdge>), element => element.TagName is "LINE" && element.GetAttribute("data-elementtype") == "edge"),
};
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="KristofferStrube.Blazor.SVGEditor" Version="0.2.0" />
<PackageReference Include="KristofferStrube.Blazor.SVGEditor" Version="0.2.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.8" />
</ItemGroup>

Expand Down

0 comments on commit 8ce27ef

Please sign in to comment.