From 2841366e2a7037d1cc3c5990124c880e6ec3fef6 Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Mon, 28 Sep 2026 10:12:38 -0400
Subject: [PATCH] Plan viewer: color edges by the actual/estimated row ratio
(#4579)
Ports PerformanceStudio's edge-accuracy coloring: the elbow connector feeding
an actual-plan operator now colors by how far its actual row count diverged
from the estimate, staying the plain default color inside a divergence band
and climbing through three tiers each way outside it. Estimated plans keep
the plain default; the tier math lives in a pure PlanEdgeColour helper shared
by both hosts.
Adds an AccuracyRatioDivergenceLimit setting (default 10, floored at 2) to
both the Darling Viewer and Lite, mirroring the PerformanceStudio setting.
---
Darling/Darling.Tests/Viewer4579Tests.cs | 154 ++++++++++++++++++
.../ViewerActualPlanFlow.cs | 2 +-
.../ViewerAppSettings.cs | 17 ++
.../ViewerServerTab.Plans.cs | 2 +-
Lite/App.xaml.cs | 13 ++
Lite/Controls/ServerTab.Plans.cs | 2 +-
Lite/Windows/PlanViewerWindow.xaml.cs | 1 +
Lite/config/settings.sample.json | 4 +
.../PlanEdgeColour.cs | 92 +++++++++++
.../PlanViewerControl.Rendering.cs | 23 ++-
.../PlanViewerControl.xaml.cs | 16 ++
11 files changed, 322 insertions(+), 4 deletions(-)
create mode 100644 Darling/Darling.Tests/Viewer4579Tests.cs
create mode 100644 PerformanceMonitor.PlanAnalysis/PlanEdgeColour.cs
diff --git a/Darling/Darling.Tests/Viewer4579Tests.cs b/Darling/Darling.Tests/Viewer4579Tests.cs
new file mode 100644
index 0000000000..136b4b7bca
--- /dev/null
+++ b/Darling/Darling.Tests/Viewer4579Tests.cs
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using PerformanceMonitor.PlanAnalysis;
+using Xunit;
+
+namespace Darling.Tests;
+
+///
+/// #4579: plan-edge coloring by the child operator's actual-vs-estimated row ratio, matching
+/// erikdarlingdata/PerformanceStudio dev's GetLinkColorBrush. is a
+/// new type, so every assertion here is a compile-only RED against dev.
+///
+public sealed class Viewer4579Tests
+{
+ private const double Limit = 10.0;
+
+ [Fact]
+ public void EstimatedPlan_AlwaysNeutral()
+ {
+ // hasActualStats = false, even with a wildly diverging ratio, stays neutral (estimated plans
+ // never get accuracy-based color).
+ Assert.Equal(PlanEdgeColourKey.Neutral, PlanEdgeColour.ForChild(false, 1_000_000, 1, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_ExactMatch_IsNeutral()
+ {
+ Assert.Equal(PlanEdgeColourKey.Neutral, PlanEdgeColour.ForChild(true, 100, 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_ZeroEstimate_ZeroActual_IsNeutral()
+ {
+ // estRows == 0 and actualRows == 0 => ratio treated as 1.0 (PS's exact fallback).
+ Assert.Equal(PlanEdgeColourKey.Neutral, PlanEdgeColour.ForChild(true, 0, 0, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_ZeroEstimate_NonZeroActual_IsFluoRed()
+ {
+ // estRows == 0 and actualRows > 0 => ratio treated as double.MaxValue, the top underestimate tier.
+ Assert.Equal(PlanEdgeColourKey.FluoRed, PlanEdgeColour.ForChild(true, 5, 0, Limit));
+ }
+
+ // ---- Underestimate side (accuracyRatio > 1: more actual rows than estimated) ----
+
+ [Theory]
+ [InlineData(9.9)] // just inside the neutral band
+ [InlineData(1.0 / 9.9)]
+ public void ActualPlan_JustInsideNeutralBand_IsNeutral(double ratio)
+ {
+ Assert.Equal(PlanEdgeColourKey.Neutral, PlanEdgeColour.ForChild(true, ratio * 100, 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_AtLimit_IsNeutral()
+ {
+ // accuracyRatio == limit is still inside the closed neutral band ([1/limit, limit]).
+ Assert.Equal(PlanEdgeColourKey.Neutral, PlanEdgeColour.ForChild(true, Limit * 100, 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_JustAboveLimit_IsLightOrange()
+ {
+ Assert.Equal(PlanEdgeColourKey.LightOrange, PlanEdgeColour.ForChild(true, Limit * 100 + 1, 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_AtLimitTimes10_IsFluoOrange()
+ {
+ Assert.Equal(PlanEdgeColourKey.FluoOrange, PlanEdgeColour.ForChild(true, Limit * 10 * 100, 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_JustBelowLimitTimes10_IsLightOrange()
+ {
+ Assert.Equal(PlanEdgeColourKey.LightOrange, PlanEdgeColour.ForChild(true, (Limit * 10 - 0.001) * 100, 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_AtLimitTimes100_IsFluoRed()
+ {
+ Assert.Equal(PlanEdgeColourKey.FluoRed, PlanEdgeColour.ForChild(true, Limit * 100 * 100, 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_JustBelowLimitTimes100_IsFluoOrange()
+ {
+ Assert.Equal(PlanEdgeColourKey.FluoOrange, PlanEdgeColour.ForChild(true, (Limit * 100 - 0.001) * 100, 100, Limit));
+ }
+
+ // ---- Overestimate side (accuracyRatio < 1: fewer actual rows than estimated) ----
+
+ [Fact]
+ public void ActualPlan_JustBelowInverseLimit_IsBlue()
+ {
+ Assert.Equal(PlanEdgeColourKey.Blue, PlanEdgeColour.ForChild(true, 100, Limit * 100 + 1, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_AtInverseLimitTimes10_IsBlue()
+ {
+ // Overestimate tiers use a strict '<' boundary (PS's exact form), so exactly at the tier
+ // boundary the ratio has not yet crossed into the next tier.
+ Assert.Equal(PlanEdgeColourKey.Blue, PlanEdgeColour.ForChild(true, 100, Limit * 10 * 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_JustAboveInverseLimitTimes10_IsBlue()
+ {
+ Assert.Equal(PlanEdgeColourKey.Blue, PlanEdgeColour.ForChild(true, 100, (Limit * 10 - 0.001) * 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_AtInverseLimitTimes100_IsLightBlue()
+ {
+ // Same strict '<' boundary behavior at the top tier.
+ Assert.Equal(PlanEdgeColourKey.LightBlue, PlanEdgeColour.ForChild(true, 100, Limit * 100 * 100, Limit));
+ }
+
+ [Fact]
+ public void ActualPlan_JustAboveInverseLimitTimes100_IsLightBlue()
+ {
+ Assert.Equal(PlanEdgeColourKey.LightBlue, PlanEdgeColour.ForChild(true, 100, (Limit * 100 - 0.001) * 100, Limit));
+ }
+
+ // ---- Divergence-limit floor ----
+
+ [Fact]
+ public void DivergenceLimit_BelowFloor_IsClampedToTwo()
+ {
+ // limit = 1 is floored to 2.0 (PS's Math.Max(2.0, ...)); a ratio of 3 is then past the floored
+ // limit, not inside a (nonsensical) [1, 1] neutral band.
+ Assert.Equal(PlanEdgeColourKey.LightOrange, PlanEdgeColour.ForChild(true, 300, 100, 1.0));
+ }
+
+ [Fact]
+ public void DefaultDivergenceLimit_MatchesPerformanceStudio()
+ {
+ Assert.Equal(10.0, PlanEdgeColour.DefaultDivergenceLimit);
+ }
+
+ [Fact]
+ public void MinDivergenceLimit_MatchesPerformanceStudio()
+ {
+ Assert.Equal(2.0, PlanEdgeColour.MinDivergenceLimit);
+ }
+}
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerActualPlanFlow.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerActualPlanFlow.cs
index eb45dacabc..16258cb414 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerActualPlanFlow.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerActualPlanFlow.cs
@@ -104,7 +104,7 @@ public static class ViewerActualPlanFlow
///
public static async Task OpenFloatingPlanAsync(Window owner, string planXml, string label, string? queryText)
{
- var viewer = new PlanViewerControl();
+ var viewer = new PlanViewerControl { AccuracyRatioDivergenceLimit = ViewerExportSettings.AccuracyRatioDivergenceLimit };
try
{
await viewer.LoadPlan(planXml, label, queryText);
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerAppSettings.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerAppSettings.cs
index ef5073bf0d..9b323b6b25 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerAppSettings.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerAppSettings.cs
@@ -94,6 +94,15 @@ public sealed class ViewerAppSettings
/// applied at startup and live-previewed from the Settings window via the shared ThemeManager.
public string ColorTheme { get; set; } = "Dark";
+ ///
+ /// How far a plan operator's actual row count may diverge from its estimate before the plan viewer
+ /// colors the edge feeding it (#4579), matching PerformanceStudio's AccuracyRatioDivergenceLimit
+ /// setting. The viewer floors this at
+ /// when it renders, so a hand-edited value below that has no effect. Mirrors Lite's
+ /// App.AccuracyRatioDivergenceLimit.
+ ///
+ public double AccuracyRatioDivergenceLimit { get; set; } = PerformanceMonitor.PlanAnalysis.PlanEdgeColour.DefaultDivergenceLimit;
+
/* ---------------- Notifications / alert thresholds (mirrors Lite App.*) ---------------- */
public bool MinimizeToTray { get; set; } = true;
@@ -203,6 +212,9 @@ internal ViewerAppSettings Normalize()
TimeDisplayMode = (TimeDisplayMode is "ServerTime" or "LocalTime" or "UTC") ? TimeDisplayMode : "ServerTime";
OverviewSortMode = (OverviewSortMode is "Cpu" or "Name") ? OverviewSortMode : "Cpu";
ColorTheme = (ColorTheme is "Dark" or "Light" or "CoolBreeze") ? ColorTheme : "Dark";
+ AccuracyRatioDivergenceLimit = AccuracyRatioDivergenceLimit >= PerformanceMonitor.PlanAnalysis.PlanEdgeColour.MinDivergenceLimit
+ ? AccuracyRatioDivergenceLimit
+ : PerformanceMonitor.PlanAnalysis.PlanEdgeColour.DefaultDivergenceLimit;
AlertCpuThreshold = Clamp(AlertCpuThreshold, 1, 100, 80);
AlertCpuMode = (AlertCpuMode is "Total" or "SqlOnly") ? AlertCpuMode : "Total";
@@ -354,7 +366,12 @@ public static void Apply(ViewerAppSettings settings)
{
ArgumentNullException.ThrowIfNull(settings);
CsvSeparator = settings.CsvSeparator;
+ AccuracyRatioDivergenceLimit = settings.AccuracyRatioDivergenceLimit;
}
+
+ /// Current plan-edge accuracy-ratio divergence limit (#4579). Defaults to
+ /// .
+ public static double AccuracyRatioDivergenceLimit { get; set; } = PerformanceMonitor.PlanAnalysis.PlanEdgeColour.DefaultDivergenceLimit;
}
///
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Plans.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Plans.cs
index ff751b4e36..d22c786b9f 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Plans.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Plans.cs
@@ -54,7 +54,7 @@ public partial class ViewerServerTab
private async Task OpenPlanTab(string planXml, string label, string? queryText = null)
{
HidePlanLoading();
- var viewer = new PlanViewerControl();
+ var viewer = new PlanViewerControl { AccuracyRatioDivergenceLimit = ViewerExportSettings.AccuracyRatioDivergenceLimit };
try
{
await viewer.LoadPlan(planXml, label, queryText);
diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs
index c5e3387d11..e7d8ce5d4e 100644
--- a/Lite/App.xaml.cs
+++ b/Lite/App.xaml.cs
@@ -110,6 +110,14 @@ being handed back the stale in-memory version. The coordinator owns the mutex +
///
public static int AutoRefreshIntervalSeconds { get; set; } = 60;
+ ///
+ /// How far a plan operator's actual row count may diverge from its estimate before the plan viewer
+ /// colors the edge feeding it (#4579), matching PerformanceStudio's AccuracyRatioDivergenceLimit
+ /// setting. The viewer floors this at
+ /// when it renders, so a hand-edited settings.json value below that has no effect.
+ ///
+ public static double AccuracyRatioDivergenceLimit { get; set; } = PerformanceMonitor.PlanAnalysis.PlanEdgeColour.DefaultDivergenceLimit;
+
/* Alert settings */
public static bool AlertsEnabled { get; set; } = true;
public static bool NotifyConnectionChanges { get; set; } = true;
@@ -987,6 +995,11 @@ default_time_range_hours has with the TimeRangeCombo restore switch. */
AutoRefreshIntervalSeconds = refreshSeconds.WholeNumber(AutoRefreshIntervalSeconds);
}
+ if (read.TryGetProperty("accuracy_ratio_divergence_limit", out var divergenceLimit))
+ {
+ AccuracyRatioDivergenceLimit = divergenceLimit.Number(AccuracyRatioDivergenceLimit, 2.0, 1000000.0);
+ }
+
/* #2444: this loader named its keys even when it had only one, which is the behaviour
LoadAlertSettings could not manage across eighty-seven. It says so through the shared
reporter instead of its own log line, so the startup dialog can name these keys beside
diff --git a/Lite/Controls/ServerTab.Plans.cs b/Lite/Controls/ServerTab.Plans.cs
index 1db3033f68..9e2fb6c566 100644
--- a/Lite/Controls/ServerTab.Plans.cs
+++ b/Lite/Controls/ServerTab.Plans.cs
@@ -252,7 +252,7 @@ private void ViewQueryStorePlanInline_Click(object sender, RoutedEventArgs e)
private async Task OpenPlanTab(string planXml, string label, string? queryText = null)
{
HidePlanLoading();
- var viewer = new PlanViewerControl();
+ var viewer = new PlanViewerControl { AccuracyRatioDivergenceLimit = App.AccuracyRatioDivergenceLimit };
try
{
/* LoadPlan parses+analyzes off the UI thread; it throws XmlException for malformed
diff --git a/Lite/Windows/PlanViewerWindow.xaml.cs b/Lite/Windows/PlanViewerWindow.xaml.cs
index 30fe0de442..d84b864a40 100644
--- a/Lite/Windows/PlanViewerWindow.xaml.cs
+++ b/Lite/Windows/PlanViewerWindow.xaml.cs
@@ -22,6 +22,7 @@ public partial class PlanViewerWindow : Window
public PlanViewerWindow()
{
InitializeComponent();
+ Viewer.AccuracyRatioDivergenceLimit = App.AccuracyRatioDivergenceLimit;
// The shared PlanViewerControl needs an explicit Cleanup() to unsubscribe ThemeManager.
Closed += (_, _) => Viewer.Cleanup();
}
diff --git a/Lite/config/settings.sample.json b/Lite/config/settings.sample.json
index aaf8a921dc..afc82ea770 100644
--- a/Lite/config/settings.sample.json
+++ b/Lite/config/settings.sample.json
@@ -158,6 +158,10 @@
"auto_refresh_enabled": true,
"auto_refresh_interval_seconds": 60, // 30 | 60 | 300
+ // How far a plan operator's actual row count may diverge from its estimate before the plan viewer
+ // colors the edge feeding it. Floored at 2.0 when the viewer renders.
+ "accuracy_ratio_divergence_limit": 10,
+
// ---- Overview and sidebar state -----------------------------------------------------------------
// Written by the app as you use it, not settings you would normally hand-edit.
"overview_sort_mode": "Cpu", // "Cpu" | "Name"; unrecognised -> "Cpu"
diff --git a/PerformanceMonitor.PlanAnalysis/PlanEdgeColour.cs b/PerformanceMonitor.PlanAnalysis/PlanEdgeColour.cs
new file mode 100644
index 0000000000..b176fbc871
--- /dev/null
+++ b/PerformanceMonitor.PlanAnalysis/PlanEdgeColour.cs
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+namespace PerformanceMonitor.PlanAnalysis;
+
+///
+/// One of the seven colors a plan-edge (elbow connector) can take, keyed by how far a child
+/// operator's actual row count diverged from its estimate. is the plain
+/// default edge color; the other six are the three-tier overestimate/underestimate ramps.
+///
+public enum PlanEdgeColourKey
+{
+ /// Estimated plan, or actual within the neutral band: the plain default edge color.
+ Neutral,
+
+ /// Underestimated (more actual rows than estimated), first tier past the limit.
+ LightOrange,
+
+ /// Underestimated, second tier (limit x 10).
+ FluoOrange,
+
+ /// Underestimated, third tier (limit x 100).
+ FluoRed,
+
+ /// Overestimated (fewer actual rows than estimated), first tier past the limit.
+ Blue,
+
+ /// Overestimated, second tier (limit x 10).
+ LightBlue,
+
+ /// Overestimated, third tier (limit x 100).
+ FluoBlue,
+}
+
+///
+/// Pure helper computing plan-edge color by the estimate-vs-actual row-count ratio of the CHILD
+/// operator feeding that edge, matching erikdarlingdata/PerformanceStudio's
+/// PlanViewerControl.Rendering.cs GetLinkColorBrush exactly (colors only actual plans;
+/// estimated plans keep the neutral default). Kept here (no WPF dependency) so a unit test can pin
+/// every tier boundary without needing PerformanceMonitor.Ui, which is net10.0-windows-only.
+///
+public static class PlanEdgeColour
+{
+ /// The floor PerformanceStudio clamps the divergence-limit setting to.
+ public const double MinDivergenceLimit = 2.0;
+
+ /// PerformanceStudio's default divergence-limit setting (before clamping).
+ public const double DefaultDivergenceLimit = 10.0;
+
+ ///
+ /// Returns the color key for the edge feeding .
+ /// is the raw setting value; this clamps it to itself, so callers
+ /// may pass the setting unclamped.
+ ///
+ public static PlanEdgeColourKey ForChild(bool hasActualStats, double actualRows, double estimateRows, double divergenceLimit)
+ {
+ if (!hasActualStats)
+ return PlanEdgeColourKey.Neutral;
+
+ divergenceLimit = System.Math.Max(MinDivergenceLimit, divergenceLimit);
+
+ var accuracyRatio = estimateRows > 0
+ ? actualRows / estimateRows
+ : (actualRows > 0 ? double.MaxValue : 1.0);
+
+ // Within the neutral band — keep the default color.
+ if (accuracyRatio >= 1.0 / divergenceLimit && accuracyRatio <= divergenceLimit)
+ return PlanEdgeColourKey.Neutral;
+
+ // Underestimated bands (accuracyRatio > 1 means more actual rows than estimated).
+ if (accuracyRatio > divergenceLimit)
+ {
+ if (accuracyRatio >= divergenceLimit * 100)
+ return PlanEdgeColourKey.FluoRed;
+ if (accuracyRatio >= divergenceLimit * 10)
+ return PlanEdgeColourKey.FluoOrange;
+ return PlanEdgeColourKey.LightOrange;
+ }
+
+ // Overestimated bands (accuracyRatio < 1 means fewer actual rows than estimated).
+ if (accuracyRatio < 1.0 / (divergenceLimit * 100))
+ return PlanEdgeColourKey.FluoBlue;
+ if (accuracyRatio < 1.0 / (divergenceLimit * 10))
+ return PlanEdgeColourKey.LightBlue;
+ return PlanEdgeColourKey.Blue;
+ }
+}
diff --git a/PerformanceMonitor.Ui/PlanViewerControl.Rendering.cs b/PerformanceMonitor.Ui/PlanViewerControl.Rendering.cs
index 49b494bb96..f31935e941 100644
--- a/PerformanceMonitor.Ui/PlanViewerControl.Rendering.cs
+++ b/PerformanceMonitor.Ui/PlanViewerControl.Rendering.cs
@@ -371,7 +371,7 @@ private WpfPath CreateElbowConnector(PlanNode parent, PlanNode child)
return new WpfPath
{
Data = geometry,
- Stroke = EdgeBrush,
+ Stroke = GetLinkColorBrush(child),
StrokeThickness = thickness,
StrokeLineJoin = PenLineJoin.Round,
ToolTip = BuildEdgeTooltipContent(child),
@@ -379,6 +379,27 @@ private WpfPath CreateElbowConnector(PlanNode parent, PlanNode child)
};
}
+ ///
+ /// Returns the brush for the edge feeding , colored by how far its actual
+ /// row count diverged from its estimate. Only actual plans get non-default colors; matches
+ /// erikdarlingdata/PerformanceStudio's GetLinkColorBrush exactly. The pure ratio-to-tier logic
+ /// lives in so it can be pinned without WPF.
+ ///
+ private SolidColorBrush GetLinkColorBrush(PlanNode child)
+ {
+ var key = PlanEdgeColour.ForChild(child.HasActualStats, child.ActualRows, child.EstimateRows, AccuracyRatioDivergenceLimit);
+ return key switch
+ {
+ PlanEdgeColourKey.LightOrange => EdgeLightOrangeBrush,
+ PlanEdgeColourKey.FluoOrange => EdgeFluoOrangeBrush,
+ PlanEdgeColourKey.FluoRed => EdgeFluoRedBrush,
+ PlanEdgeColourKey.Blue => EdgeBlueBrush,
+ PlanEdgeColourKey.LightBlue => EdgeLightBlueBrush,
+ PlanEdgeColourKey.FluoBlue => EdgeFluoBlueBrush,
+ _ => EdgeBrush,
+ };
+ }
+
private Border BuildEdgeTooltipContent(PlanNode child)
{
var grid = new Grid { MinWidth = 240 };
diff --git a/PerformanceMonitor.Ui/PlanViewerControl.xaml.cs b/PerformanceMonitor.Ui/PlanViewerControl.xaml.cs
index d7ada0dabc..e8dee688cb 100644
--- a/PerformanceMonitor.Ui/PlanViewerControl.xaml.cs
+++ b/PerformanceMonitor.Ui/PlanViewerControl.xaml.cs
@@ -39,6 +39,22 @@ public partial class PlanViewerControl : UserControl
private static readonly SolidColorBrush EdgeBrush = new(Color.FromRgb(0x6B, 0x72, 0x80));
private static readonly SolidColorBrush OrangeBrush = new(Color.FromRgb(0xFF, 0xB3, 0x47));
+ // Edge accuracy-ratio colors, matching erikdarlingdata/PerformanceStudio's PlanViewerControl exactly.
+ private static readonly SolidColorBrush EdgeLightOrangeBrush = new(Color.FromRgb(0xFF, 0xB7, 0x4D));
+ private static readonly SolidColorBrush EdgeFluoOrangeBrush = new(Color.FromRgb(0xFF, 0x8C, 0x00));
+ private static readonly SolidColorBrush EdgeFluoRedBrush = new(Color.FromRgb(0xFF, 0x17, 0x44));
+ private static readonly SolidColorBrush EdgeBlueBrush = new(Color.FromRgb(0x42, 0x8B, 0xCA));
+ private static readonly SolidColorBrush EdgeLightBlueBrush = new(Color.FromRgb(0x64, 0xB5, 0xF6));
+ private static readonly SolidColorBrush EdgeFluoBlueBrush = new(Color.FromRgb(0x00, 0xE5, 0xFF));
+
+ ///
+ /// How far a child operator's actual row count may diverge from its estimate before the edge feeding
+ /// it is colored; matches PerformanceStudio's AccuracyRatioDivergenceLimit setting (default 10,
+ /// floored at when applied). Hosts set this from their
+ /// own settings store before rendering.
+ ///
+ public double AccuracyRatioDivergenceLimit { get; set; } = PlanEdgeColour.DefaultDivergenceLimit;
+
// Theme-aware brushes resolved at call time from Application.Resources
private SolidColorBrush TooltipBgBrush =>
(TryFindResource("PlanTooltipBgBrush") as SolidColorBrush) ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1D, 0x23));