From 4ac9168dfb0d8ebc4cd8fe817eaea4b835df4e11 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Fri, 12 Sep 2025 23:11:48 +1000 Subject: [PATCH] Add "Direct references only" option to graph control The size of the target graph is often so large that related nodes aren't visible within the viewport. This change adds a checkbox to the toolbar that causes nodes that aren't directly referenced from the selected node. Clicking on one of those nodes will refocus the graph around that node, adding any newly needed nodes and hiding any no longer directly referenced. --- .../Controls/GraphControl.cs | 127 +++++++++++++++++- .../Controls/GraphHostControl.cs | 11 ++ 2 files changed, 133 insertions(+), 5 deletions(-) diff --git a/src/StructuredLogViewer/Controls/GraphControl.cs b/src/StructuredLogViewer/Controls/GraphControl.cs index 0aa229b92..8aa0f9a58 100644 --- a/src/StructuredLogViewer/Controls/GraphControl.cs +++ b/src/StructuredLogViewer/Controls/GraphControl.cs @@ -116,6 +116,34 @@ public bool HideTransitiveEdges } } + private bool directReferencesOnly; + private Vertex focusVertex; + public bool DirectReferencesOnly + { + get => directReferencesOnly; + set + { + if (directReferencesOnly == value) + { + return; + } + + directReferencesOnly = value; + + // If enabling direct references only mode, set focus to first selected vertex + if (value && selectedVertices.Any()) + { + focusVertex = selectedVertices.First(); + } + else if (!value) + { + focusVertex = null; + } + + Redraw(); + } + } + private bool layerByDepth; public bool LayerByDepth { @@ -171,8 +199,16 @@ private void Populate() return; } - var maxHeight = graph.Vertices.Max(g => g.Height); - var maxDepth = graph.Vertices.Max(g => g.Depth); + // Get vertices to display based on filtering mode + var verticesToDisplay = GetVerticesToDisplay(); + + if (!verticesToDisplay.Any()) + { + return; + } + + var maxHeight = verticesToDisplay.Max(g => g.Height); + var maxDepth = verticesToDisplay.Max(g => g.Depth); var primaryOrientation = Orientation.Vertical; var secondaryOrientation = Orientation.Horizontal; if (Horizontal) @@ -189,7 +225,7 @@ private void Populate() groupBy = v => maxDepth - v.Depth; } - var groups = graph.Vertices.GroupBy(groupBy).OrderBy(g => g.Key).ToArray(); + var groups = verticesToDisplay.GroupBy(groupBy).OrderBy(g => g.Key).ToArray(); if (Inverted) { groups = groups.Reverse().ToArray(); @@ -211,6 +247,14 @@ private void Populate() var paddingHeight = Math.Pow(vertex.InDegree, 0.6); var opacity = vertex.InDegree > 1 ? 0.9 : 0.5; + + // Highlight the focus vertex if in direct references mode + if (DirectReferencesOnly && vertex == focusVertex) + { + opacity = 1.0; + background = DarkTheme ? Color.FromRgb(255, 140, 0) : Color.FromRgb(255, 255, 0); // Orange/Yellow highlight + } + var vertexControl = new TextBlock() { Text = vertex.Title.TrimQuotes(), @@ -241,6 +285,13 @@ private void Populate() SelectVertices([vertex]); } } + + // In direct references mode, clicking a vertex recenters the graph on that vertex + if (DirectReferencesOnly && vertex != focusVertex) + { + focusVertex = vertex; + Redraw(); + } }; layerPanel.Children.Add(vertexControl); } @@ -251,6 +302,49 @@ private void Populate() SelectVertices(selectedVertices.ToArray()); } + private IEnumerable GetVerticesToDisplay() + { + if (!DirectReferencesOnly) + { + return graph.Vertices; + } + + // If no focus vertex is set, use the first vertex with the highest in-degree (most connected) + // or fall back to the first vertex + if (focusVertex == null) + { + focusVertex = graph.Vertices + .OrderByDescending(v => v.InDegree + v.Outgoing.Count()) + .ThenBy(v => v.Title) + .FirstOrDefault(); + + if (focusVertex == null) + { + return Enumerable.Empty(); + } + } + + // In direct references mode, show only the focus vertex and its directly connected vertices + var directlyConnected = new HashSet(); + + // Add the focus vertex itself + directlyConnected.Add(focusVertex); + + // Add vertices that the focus vertex depends on (outgoing connections) + foreach (var outgoing in focusVertex.Outgoing) + { + directlyConnected.Add(outgoing); + } + + // Add vertices that depend on the focus vertex (incoming connections) + foreach (var incoming in focusVertex.Incoming) + { + directlyConnected.Add(incoming); + } + + return directlyConnected; + } + private Color ComputeBackground(int depth) { byte ratio, halfratio; @@ -378,7 +472,7 @@ void SelectVertices(IEnumerable vertices) selectedVertices.UnionWith(vertices); - var controls = vertices.Select(GetControl).ToArray(); + var controls = vertices.Select(GetControl).Where(c => c != null).ToArray(); SelectControls(controls); } @@ -481,6 +575,8 @@ void SelectControl(FrameworkElement vertexControl) private void AddIncomingEdges(Rect destinationRect, Vertex destinationVertex) { + var visibleVertices = DirectReferencesOnly ? GetVerticesToDisplay().ToHashSet() : null; + foreach (var incoming in destinationVertex.Incoming) { if (HideTransitiveEdges && @@ -490,6 +586,12 @@ private void AddIncomingEdges(Rect destinationRect, Vertex destinationVertex) continue; } + // In direct references mode, only show edges to visible vertices + if (DirectReferencesOnly && visibleVertices != null && !visibleVertices.Contains(incoming)) + { + continue; + } + if (GetControl(incoming) is { } sourceControl) { var sourceRect = GetRectOnCanvas(sourceControl); @@ -502,9 +604,16 @@ private void AddIncomingEdges(Rect destinationRect, Vertex destinationVertex) private void AddOutgoingEdges(Rect sourceRect, Vertex node) { IEnumerable list = HideTransitiveEdges ? node.NonRedundantOutgoing : node.Outgoing; + var visibleVertices = DirectReferencesOnly ? GetVerticesToDisplay().ToHashSet() : null; foreach (var outgoing in list) { + // In direct references mode, only show edges to visible vertices + if (DirectReferencesOnly && visibleVertices != null && !visibleVertices.Contains(outgoing)) + { + continue; + } + if (GetControl(outgoing) is { } destinationControl) { var destinationRect = GetRectOnCanvas(destinationControl); @@ -553,7 +662,15 @@ public void Locate(string text) foundControl.BringIntoView(); } - SelectVertices(found.Select(GetVertex).ToArray()); + var foundVertices = found.Select(GetVertex).ToArray(); + SelectVertices(foundVertices); + + // In direct references mode, set focus to the first found vertex + if (DirectReferencesOnly && foundVertices.Any()) + { + focusVertex = foundVertices.First(); + Redraw(); + } } private FrameworkElement FindControlByText(string text) diff --git a/src/StructuredLogViewer/Controls/GraphHostControl.cs b/src/StructuredLogViewer/Controls/GraphHostControl.cs index e2336f905..b6a257fde 100644 --- a/src/StructuredLogViewer/Controls/GraphHostControl.cs +++ b/src/StructuredLogViewer/Controls/GraphHostControl.cs @@ -160,6 +160,13 @@ private void Initialize() Margin = new Thickness(0, 0, 8, 0) }; + var directReferencesOnlyCheck = new CheckBox + { + Content = "Direct references only", + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0) + }; + var helpButton = new Button { Content = "Help", @@ -242,6 +249,7 @@ private void Initialize() toolbar.Children.Add(showTextButton); toolbar.Children.Add(transitiveReduceCheck); + toolbar.Children.Add(directReferencesOnlyCheck); toolbar.Children.Add(depthCheckbox); toolbar.Children.Add(horizontalCheckbox); toolbar.Children.Add(invertedCheckbox); @@ -298,6 +306,9 @@ private void Initialize() transitiveReduceCheck.Checked += (s, e) => graphControl.HideTransitiveEdges = true; transitiveReduceCheck.Unchecked += (s, e) => graphControl.HideTransitiveEdges = false; + directReferencesOnlyCheck.Checked += (s, e) => graphControl.DirectReferencesOnly = true; + directReferencesOnlyCheck.Unchecked += (s, e) => graphControl.DirectReferencesOnly = false; + void Locate() { var text = searchTextBox.Text;