Skip to content

Commit 70d28e6

Browse files
authored
Features/colored links (#289)
* AppSettings (AppSettingsService.cs) • Added AccuracyRatioDivergenceLimit property (default: 10), persisted as "accuracy_ratio_divergence_limit" in the JSON settings file. PlanViewerControl (PlanViewerControl.axaml.cs) • 6 new link color brushes for dark theme: • Underestimated (more actual than estimated): Blue → Light Blue → Fluo Blue • Overestimated (fewer actual than estimated): Light Orange → Fluo Orange → Fluo Red • GetLinkColorBrush(PlanNode, double) method — maps accuracy ratio to a color band: • [0, 1/(limit×100)) → Fluo Blue • [1/(limit×100), 1/(limit×10)) → Light Blue • [1/(limit×10), 1/limit) → Blue • [1/limit, limit] → Default edge color (neutral) • (limit, limit×10) → Light Orange • [limit×10, limit×100) → Fluo Orange • [limit×100, +∞) → Fluo Red • CreateElbowConnector(PlanNode, PlanNode) now uses GetLinkColorBrush(PlanNode, double) instead of the static EdgeBrush, but only colors links differently for actual plans (estimated plans keep the default). * minimap colored links * Internal Code review and some fixes * AppSettingsService.Load().AccuracyRatioDivergenceLimit is no more done per node but only once
1 parent 52d11d5 commit 70d28e6

2 files changed

Lines changed: 75 additions & 17 deletions

File tree

‎src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs‎

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ public partial class PlanViewerControl : UserControl
9393
private static readonly SolidColorBrush OrangeBrush = new(Colors.Orange);
9494
private static readonly SolidColorBrush MinimapExpensiveNodeBgBrush = new(Color.FromArgb(0x60, 0xE5, 0x73, 0x73));
9595

96+
// Link accuracy coloring brushes (Dark theme)
97+
private static readonly SolidColorBrush LinkFluoBlueBrush = new(Color.FromRgb(0x00, 0xE5, 0xFF));
98+
private static readonly SolidColorBrush LinkLightBlueBrush = new(Color.FromRgb(0x64, 0xB5, 0xF6));
99+
private static readonly SolidColorBrush LinkBlueBrush = new(Color.FromRgb(0x42, 0x8B, 0xCA));
100+
private static readonly SolidColorBrush LinkLightOrangeBrush = new(Color.FromRgb(0xFF, 0xB7, 0x4D));
101+
private static readonly SolidColorBrush LinkFluoOrangeBrush = new(Color.FromRgb(0xFF, 0x8C, 0x00));
102+
private static readonly SolidColorBrush LinkFluoRedBrush = new(Color.FromRgb(0xFF, 0x17, 0x44));
103+
96104

97105
// Track all property section grids for synchronized column resize
98106
private readonly List<ColumnDefinition> _sectionLabelColumns = new();
@@ -395,12 +403,13 @@ private void RenderStatement(PlanStatement statement)
395403
PlanCanvas.Height = height;
396404

397405
// Render edges first (behind nodes)
398-
RenderEdges(statement.RootNode);
406+
var divergenceLimit = Math.Max(2.0, AppSettingsService.Load().AccuracyRatioDivergenceLimit);
407+
RenderEdges(statement.RootNode, divergenceLimit);
399408

400409
// Render nodes — pass total warning count to root node for badge
401410
var allWarnings = new List<PlanWarning>();
402411
CollectWarnings(statement.RootNode, allWarnings);
403-
RenderNodes(statement.RootNode, allWarnings.Count);
412+
RenderNodes(statement.RootNode, divergenceLimit, allWarnings.Count);
404413

405414
// Update banners
406415
ShowMissingIndexes(statement.MissingIndexes);
@@ -425,18 +434,18 @@ private void RenderStatement(PlanStatement statement)
425434

426435
#region Node Rendering
427436

428-
private void RenderNodes(PlanNode node, int totalWarningCount = -1)
437+
private void RenderNodes(PlanNode node, double divergenceLimit, int totalWarningCount = -1)
429438
{
430-
var visual = CreateNodeVisual(node, totalWarningCount);
439+
var visual = CreateNodeVisual(node, divergenceLimit, totalWarningCount);
431440
Canvas.SetLeft(visual, node.X);
432441
Canvas.SetTop(visual, node.Y);
433442
PlanCanvas.Children.Add(visual);
434443

435444
foreach (var child in node.Children)
436-
RenderNodes(child);
445+
RenderNodes(child, divergenceLimit);
437446
}
438447

439-
private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1)
448+
private Border CreateNodeVisual(PlanNode node, double divergenceLimit, int totalWarningCount = -1)
440449
{
441450
var isExpensive = node.IsExpensive;
442451

@@ -648,10 +657,10 @@ private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1)
648657
HorizontalAlignment = HorizontalAlignment.Center
649658
});
650659

651-
// Actual rows of Estimated rows (accuracy %) -- red if off by 10x+
660+
// Actual rows of Estimated rows (accuracy %) -- red if off by divergence limit
652661
var estRows = node.EstimateRows;
653662
var accuracyRatio = estRows > 0 ? node.ActualRows / estRows : (node.ActualRows > 0 ? double.MaxValue : 1.0);
654-
IBrush rowBrush = (accuracyRatio < 0.1 || accuracyRatio > 10.0) ? OrangeRedBrush : fgBrush;
663+
IBrush rowBrush = (accuracyRatio < 1.0 / divergenceLimit || accuracyRatio > divergenceLimit) ? OrangeRedBrush : fgBrush;
655664
var accuracy = estRows > 0
656665
? $" ({accuracyRatio * 100:F0}%)"
657666
: "";
@@ -719,18 +728,55 @@ private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1)
719728

720729
#region Edge Rendering
721730

722-
private void RenderEdges(PlanNode node)
731+
private void RenderEdges(PlanNode node, double divergenceLimit)
723732
{
724733
foreach (var child in node.Children)
725734
{
726-
var path = CreateElbowConnector(node, child);
735+
var path = CreateElbowConnector(node, child, divergenceLimit);
727736
PlanCanvas.Children.Add(path);
728737

729-
RenderEdges(child);
738+
RenderEdges(child, divergenceLimit);
739+
}
740+
}
741+
742+
/// <summary>
743+
/// Returns a color brush for a link based on the accuracy ratio of the child node.
744+
/// Only applies to actual plans; estimated plans use the default edge brush.
745+
/// </summary>
746+
private static IBrush GetLinkColorBrush(PlanNode child, double divergenceLimit)
747+
{
748+
if (!child.HasActualStats)
749+
return EdgeBrush;
750+
751+
divergenceLimit = Math.Max(2.0, divergenceLimit);
752+
var estRows = child.EstimateRows;
753+
var accuracyRatio = estRows > 0
754+
? child.ActualRows / estRows
755+
: (child.ActualRows > 0 ? double.MaxValue : 1.0);
756+
757+
// Within the neutral band — keep default color
758+
if (accuracyRatio >= 1.0 / divergenceLimit && accuracyRatio <= divergenceLimit)
759+
return EdgeBrush;
760+
761+
// Underestimated bands (accuracyRatio > 1 means more actual rows than estimated)
762+
if (accuracyRatio > divergenceLimit)
763+
{
764+
if (accuracyRatio >= divergenceLimit * 100)
765+
return LinkFluoRedBrush;
766+
if (accuracyRatio >= divergenceLimit * 10)
767+
return LinkFluoOrangeBrush;
768+
return LinkLightOrangeBrush;
730769
}
770+
771+
// Overestimated bands (accuracyRatio < 1 means fewer actual rows than estimated)
772+
if (accuracyRatio < 1.0 / (divergenceLimit * 100))
773+
return LinkFluoBlueBrush;
774+
if (accuracyRatio < 1.0 / (divergenceLimit * 10))
775+
return LinkLightBlueBrush;
776+
return LinkBlueBrush;
731777
}
732778

733-
private AvaloniaPath CreateElbowConnector(PlanNode parent, PlanNode child)
779+
private AvaloniaPath CreateElbowConnector(PlanNode parent, PlanNode child, double divergenceLimit)
734780
{
735781
var parentRight = parent.X + PlanLayoutEngine.NodeWidth;
736782
var parentCenterY = parent.Y + PlanLayoutEngine.GetNodeHeight(parent) / 2;
@@ -754,10 +800,12 @@ private AvaloniaPath CreateElbowConnector(PlanNode parent, PlanNode child)
754800
figure.Segments.Add(new LineSegment { Point = new Point(childLeft, childCenterY) });
755801
geometry.Figures!.Add(figure);
756802

803+
var linkBrush = GetLinkColorBrush(child, divergenceLimit);
804+
757805
var path = new AvaloniaPath
758806
{
759807
Data = geometry,
760-
Stroke = EdgeBrush,
808+
Stroke = linkBrush,
761809
StrokeThickness = thickness,
762810
StrokeJoin = PenLineJoin.Round
763811
};
@@ -3565,7 +3613,8 @@ private void RenderMinimap()
35653613
RenderMinimapBranches(_currentStatement.RootNode, scale);
35663614

35673615
// Render edges
3568-
RenderMinimapEdges(_currentStatement.RootNode, scale);
3616+
var minimapDivergenceLimit = Math.Max(2.0, AppSettingsService.Load().AccuracyRatioDivergenceLimit);
3617+
RenderMinimapEdges(_currentStatement.RootNode, scale, minimapDivergenceLimit);
35693618

35703619
// Render nodes
35713620
RenderMinimapNodes(_currentStatement.RootNode, scale);
@@ -3629,7 +3678,7 @@ private static void CollectSubtreeBounds(PlanNode node, ref double minX, ref dou
36293678
CollectSubtreeBounds(child, ref minX, ref minY, ref maxX, ref maxY);
36303679
}
36313680

3632-
private void RenderMinimapEdges(PlanNode node, double scale)
3681+
private void RenderMinimapEdges(PlanNode node, double scale, double divergenceLimit)
36333682
{
36343683
foreach (var child in node.Children)
36353684
{
@@ -3651,16 +3700,18 @@ private void RenderMinimapEdges(PlanNode node, double scale)
36513700
figure.Segments.Add(new LineSegment { Point = new Point(childLeft, childCenterY) });
36523701
geometry.Figures!.Add(figure);
36533702

3703+
var linkBrush = GetLinkColorBrush(child, divergenceLimit);
3704+
36543705
var path = new AvaloniaPath
36553706
{
36563707
Data = geometry,
3657-
Stroke = EdgeBrush,
3708+
Stroke = linkBrush,
36583709
StrokeThickness = thickness,
36593710
StrokeJoin = PenLineJoin.Round
36603711
};
36613712
MinimapCanvas.Children.Add(path);
36623713

3663-
RenderMinimapEdges(child, scale);
3714+
RenderMinimapEdges(child, scale, divergenceLimit);
36643715
}
36653716
}
36663717

‎src/PlanViewer.App/Services/AppSettingsService.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,11 @@ internal sealed class AppSettings
119119
/// </summary>
120120
[JsonPropertyName("query_store_slicer_days")]
121121
public int QueryStoreSlicerDays { get; set; } = 30;
122+
123+
/// <summary>
124+
/// Divergence limit for accuracy ratio coloring on plan links. Default 10.
125+
/// Links with accuracy ratio between 1/limit and limit keep the default edge color.
126+
/// </summary>
127+
[JsonPropertyName("accuracy_ratio_divergence_limit")]
128+
public double AccuracyRatioDivergenceLimit { get; set; } = 10;
122129
}

0 commit comments

Comments
 (0)