diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1d5d2a2..d2a5a00 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - 0.3.1 + 0.3.2 Tony Redondo, Grégory Léocadie net6.0;net7.0;net8.0;net9.0 enable diff --git a/src/TimeItSharp.Common/Configuration/Config.cs b/src/TimeItSharp.Common/Configuration/Config.cs index 5fb5eb6..8282998 100644 --- a/src/TimeItSharp.Common/Configuration/Config.cs +++ b/src/TimeItSharp.Common/Configuration/Config.cs @@ -91,11 +91,11 @@ public Config() ProcessFailedDataPoints = false; ShowStdOutForFirstRun = false; DebugMode = false; - AcceptableRelativeWidth = 0.007; + AcceptableRelativeWidth = 0.006; ConfidenceLevel = 0.95; MaximumDurationInMinutes = 45; EvaluationInterval = 10; - MinimumErrorReduction = 0.0005; + MinimumErrorReduction = 0.001; } public static Config LoadConfiguration(string filePath) diff --git a/src/TimeItSharp.Common/Exporters/ConsoleExporter.cs b/src/TimeItSharp.Common/Exporters/ConsoleExporter.cs index 3c47678..6a1f659 100644 --- a/src/TimeItSharp.Common/Exporters/ConsoleExporter.cs +++ b/src/TimeItSharp.Common/Exporters/ConsoleExporter.cs @@ -33,10 +33,11 @@ public void Export(TimeitResult results) AnsiConsole.MarkupLine("[aqua bold underline]### Results (last 10):[/]"); var resultsTable = new Table() .MarkdownBorder(); - + // Add columns - resultsTable.AddColumns(results.Scenarios.Select(r => new TableColumn($"[dodgerblue1 bold]{r.Name}[/]").Centered()).ToArray()); - + resultsTable.AddColumns(results.Scenarios + .Select(r => new TableColumn($"[dodgerblue1 bold]{r.Name}[/]").Centered()).ToArray()); + // Add rows var minDurationCount = Math.Min(results.Scenarios.Select(r => r.Durations.Count).Min(), 10); for (var i = minDurationCount; i > 0; i--) @@ -47,14 +48,14 @@ public void Export(TimeitResult results) { return Math.Round(Utils.FromNanosecondsToMilliseconds(r.Durations[^i]), 3) + "ms"; } - + return "-"; }).ToArray()); } - + // Write table AnsiConsole.Write(resultsTable); - + // **************************************** // Outliers table var maxOutliersCount = Math.Min(results.Scenarios.Select(r => r.Outliers.Count).Max(), 5); @@ -127,7 +128,7 @@ public void Export(TimeitResult results) columnList.Add($"[dodgerblue1 bold]{additionalMetric.Key}[/]"); } } - + // Add columns summaryTable.AddColumns(columnList.ToArray()); @@ -138,7 +139,9 @@ public void Export(TimeitResult results) var totalNum = result.MetricsData.Count; if (totalNum > 0) { - var outliersValue = result.Outliers.Count > 0 ? $"{result.Outliers.Count} {{{Math.Round(result.OutliersThreshold, 3)}}}" : "0"; + var outliersValue = result.Outliers.Count > 0 + ? $"{result.Outliers.Count} {{{Math.Round(result.OutliersThreshold, 3)}}}" + : "0"; var rowList = new List { $"[aqua underline]{result.Name} [[N={result.Count}]][/]", @@ -147,12 +150,12 @@ public void Export(TimeitResult results) $"[aqua]{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Stdev), 3)}ms[/]", $"[aqua]{Math.Round(Utils.FromNanosecondsToMilliseconds(result.StdErr), 3)}ms[/]", $"[aqua]{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Median), 3)}ms[/]", - Math.Abs(result.Min - result.Max) > 0.0001 ? - $"[aqua][[{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Min), 3)} - {Math.Round(Utils.FromNanosecondsToMilliseconds(result.Max), 3)}]] ms[/]" : - $"[aqua]{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Min), 3)}ms[/]", - Math.Abs(result.Ci95[0] - result.Ci95[1]) > 0.0001 ? - $"[aqua][[{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[0]), 3)} - {Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[1]), 3)}]] ms[/]" : - $"[aqua]{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[0]), 3)}ms[/]", + Math.Abs(result.Min - result.Max) > 0.0001 + ? $"[aqua][[{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Min), 3)} - {Math.Round(Utils.FromNanosecondsToMilliseconds(result.Max), 3)}]] ms[/]" + : $"[aqua]{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Min), 3)}ms[/]", + Math.Abs(result.Ci95[0] - result.Ci95[1]) > 0.0001 + ? $"[aqua][[{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[0]), 3)} - {Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[1]), 3)}]] ms[/]" + : $"[aqua]{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[0]), 3)}ms[/]", $"[aqua]{outliersValue}[/]" }; @@ -184,7 +187,7 @@ public void Export(TimeitResult results) metricsThreshold += 0.1; } - + var mMean = itemResult.Mean(); var mMedian = itemResult.Median(); @@ -211,15 +214,17 @@ public void Export(TimeitResult results) Math.Round(mStdDev, 3).ToString(CultureInfo.InvariantCulture), Math.Round(mStdErr, 3).ToString(CultureInfo.InvariantCulture), Math.Round(mMedian, 3).ToString(CultureInfo.InvariantCulture), - Math.Abs(mMin - mMax) > 0.0001 ? - $"[[{Math.Round(mMin, 3).ToString(CultureInfo.InvariantCulture)} - {Math.Round(mMax, 3).ToString(CultureInfo.InvariantCulture)}]]" : - Math.Round(mMin, 3).ToString(CultureInfo.InvariantCulture), - Math.Abs(ci95[0] - ci95[1]) > 0.0001 ? - $"[[{Math.Round(ci95[0], 3).ToString(CultureInfo.InvariantCulture)} - {Math.Round(ci95[1], 3).ToString(CultureInfo.InvariantCulture)}]]" : - Math.Round(ci95[0], 3).ToString(CultureInfo.InvariantCulture), - (metricsOutliers.Count == 0 ? "0" : metricsOutliers.Count + " {" + Math.Round(metricsThreshold, 3) + "}")); + Math.Abs(mMin - mMax) > 0.0001 + ? $"[[{Math.Round(mMin, 3).ToString(CultureInfo.InvariantCulture)} - {Math.Round(mMax, 3).ToString(CultureInfo.InvariantCulture)}]]" + : Math.Round(mMin, 3).ToString(CultureInfo.InvariantCulture), + Math.Abs(ci95[0] - ci95[1]) > 0.0001 + ? $"[[{Math.Round(ci95[0], 3).ToString(CultureInfo.InvariantCulture)} - {Math.Round(ci95[1], 3).ToString(CultureInfo.InvariantCulture)}]]" + : Math.Round(ci95[0], 3).ToString(CultureInfo.InvariantCulture), + (metricsOutliers.Count == 0 + ? "0" + : metricsOutliers.Count + " {" + Math.Round(metricsThreshold, 3) + "}")); } - + if (resultsList.Count - idx > 1) { summaryTable.AddEmptyRow(); @@ -227,28 +232,32 @@ public void Export(TimeitResult results) } else { - var outliersValue = result.Outliers.Count > 0 ? $"{result.Outliers.Count} {{{Math.Round(result.OutliersThreshold, 3)}}}" : "0"; + var outliersValue = result.Outliers.Count > 0 + ? $"{result.Outliers.Count} {{{Math.Round(result.OutliersThreshold, 3)}}}" + : "0"; var rowList = new List { - $"{result.Name}", + $"{result.Name} [[N={result.Count}]]", $"{(result.Status == Status.Passed ? "[aqua]Passed" : "[red]Failed")}[/]", $"{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Mean), 3)}ms", $"{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Stdev), 3)}ms", $"{Math.Round(Utils.FromNanosecondsToMilliseconds(result.StdErr), 3)}ms", $"{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Median), 3)}ms", - Math.Abs(result.Min - result.Max) > 0.0001 ? - $"[[{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Min), 3)} - {Math.Round(Utils.FromNanosecondsToMilliseconds(result.Max), 3)}]] ms" : - $"{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Min), 3)}ms", - Math.Abs(result.Ci95[0] - result.Ci95[1]) > 0.0001 ? - $"[[{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[0]), 3)} - {Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[1]), 3)}]] ms" : - $"{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[0]), 3)}ms", + Math.Abs(result.Min - result.Max) > 0.0001 + ? $"[[{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Min), 3)} - {Math.Round(Utils.FromNanosecondsToMilliseconds(result.Max), 3)}]] ms" + : $"{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Min), 3)}ms", + Math.Abs(result.Ci95[0] - result.Ci95[1]) > 0.0001 + ? $"[[{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[0]), 3)} - {Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[1]), 3)}]] ms" + : $"{Math.Round(Utils.FromNanosecondsToMilliseconds(result.Ci95[0]), 3)}ms", $"{outliersValue}" }; foreach (var additionalMetric in additionalMetrics) { var metricValue = additionalMetric.FirstOrDefault(item => item.ScenarioResult == result); - rowList.Add(metricValue is null ? "-" : Math.Round(metricValue.Value, 3).ToString(CultureInfo.InvariantCulture)); + rowList.Add(metricValue is null + ? "-" + : Math.Round(metricValue.Value, 3).ToString(CultureInfo.InvariantCulture)); } summaryTable.AddRow(rowList.ToArray()); @@ -258,8 +267,8 @@ public void Export(TimeitResult results) // Write table AnsiConsole.Write(summaryTable); AnsiConsole.WriteLine(); - - + + // ****************************** // Write overhead table @@ -292,7 +301,8 @@ public void Export(TimeitResult results) } else { - row.Add($"{value.OverheadPercentage.ToString(CultureInfo.InvariantCulture)}% ({Math.Round(Utils.FromNanosecondsToMilliseconds(value.DeltaValue), 3)}ms)"); + row.Add( + $"{value.OverheadPercentage.ToString(CultureInfo.InvariantCulture)}% ({Math.Round(Utils.FromNanosecondsToMilliseconds(value.DeltaValue), 3)}ms)"); } } @@ -391,7 +401,7 @@ static void GenerateDistributionChart(Dictionary dataSer var binSize = range / numBins; // Determine the number of decimal places based on binSize - int decimalPlaces = binSize >= 1 ? 1 : (int)Math.Ceiling(-Math.Log10(binSize)) + 1; + var decimalPlaces = binSize >= 1 ? 1 : (int)Math.Ceiling(-Math.Log10(binSize)) + 1; // Create bin edges without rounding var binEdges = new List(); @@ -422,17 +432,24 @@ static void GenerateDistributionChart(Dictionary dataSer } } - // Generate bin ranges for display, applying rounding only here - var binRanges = new List>(); + // Simplified overlapping detection + var overlappingBinsThreshold = 4; // Set your desired threshold here + var overlappingBinsCount = 0; + for (int i = 0; i < numBins; i++) { - var start = Math.Round(binEdges[i], decimalPlaces); - var end = Math.Round(binEdges[i + 1], decimalPlaces); - binRanges.Add(Tuple.Create(start, end)); + var seriesWithCounts = 0; + foreach (var bins in binsPerSeries.Values) + { + if (bins[i] > 0) + seriesWithCounts++; + } + + if (seriesWithCounts > 1) + overlappingBinsCount++; } - // Find the maximum bin count across all series for normalizing the bars - var maxBinCount = binsPerSeries.Values.SelectMany(k => k).Max(); + var plotSeparately = overlappingBinsCount < overlappingBinsThreshold; // Assign unique characters to each series for differentiation var seriesChars = new Dictionary(); @@ -450,113 +467,259 @@ static void GenerateDistributionChart(Dictionary dataSer // Assign colors to each series var seriesColors = new Dictionary(); var availableColors = new[] { "red", "green", "blue", "yellow", "magenta", "cyan", "white" }; - int colorIndex = 0; + var colorIndex = 0; foreach (var seriesLabel in scaledDataSeriesDict.Keys) { seriesColors[seriesLabel] = availableColors[colorIndex % availableColors.Length]; colorIndex++; } - // Generate the distribution chart - var labelWidth = 27; // Adjust as necessary - var barMaxLength = 40; // Maximum length of the bar - - var formatStr = "F" + decimalPlaces; // Format string for decimal places - - for (var i = 0; i < numBins; i++) + if (plotSeparately) { - var start = binRanges[i].Item1; - var end = binRanges[i].Item2; + // Plot histograms separately for each series + foreach (var seriesLabel in scaledDataSeriesDict.Keys) + { + var data = scaledDataSeriesDict[seriesLabel]; + // Compute minData and maxData for this series + var seriesMinData = data.Min(); + var seriesMaxData = data.Max(); - // Format the bin range string - var startStr = (start.ToString(formatStr) + unit).PadLeft(10); - var endStr = (end.ToString(formatStr) + unit).PadRight(10); - var rangeStr = $"{startStr} - {endStr}"; - rangeStr = rangeStr.PadLeft(labelWidth); + // Determine unit and scale for this series based on its data + var seriesUnit = unit; + var seriesScale = 1.0d; - var seriesCount = scaledDataSeriesDict.Keys.Count; - var seriesIndex = 0; + // Recalculate bin size and edges for this series + var seriesRange = seriesMaxData - seriesMinData; - foreach (var seriesLabel in scaledDataSeriesDict.Keys) - { - var count = binsPerSeries[seriesLabel][i]; - var maxCount = maxBinCount; - var barLength = maxCount > 0 ? (int)Math.Round((double)count / maxCount * barMaxLength) : 0; - var barChar = seriesChars[seriesLabel]; - var barColor = seriesColors[seriesLabel]; - var bar = new string(barChar, barLength); + // Avoid division by zero if all data points are equal + if (seriesRange == 0) + { + seriesRange = 1; + } + + var seriesBinSize = seriesRange / numBins; - var linePrefix = string.Empty.PadLeft(labelWidth + 1); + // Determine the number of decimal places based on binSize + var seriesDecimalPlaces = seriesBinSize >= 1 ? 1 : (int)Math.Ceiling(-Math.Log10(seriesBinSize)) + 1; - if (seriesCount == 1) + // Create bin edges without rounding + var seriesBinEdges = new List(); + for (int i = 0; i <= numBins; i++) // Need numBins + 1 edges { - linePrefix = rangeStr + " ├ "; + seriesBinEdges.Add(seriesMinData + seriesBinSize * i); } - else if (seriesIndex == 0) + + // Initialize bin counts + var seriesBins = new int[numBins]; + + // Count data points in bins + foreach (var dataPoint in data) { - if (seriesCount == 2) - { - linePrefix = rangeStr + " ┌ "; - } - else - { - linePrefix += "┌ "; - } + var binIndex = (int)((dataPoint - seriesMinData) / seriesBinSize); + if (binIndex >= numBins) binIndex = numBins - 1; // Include the maximum in the last bin + seriesBins[binIndex]++; } - else if (seriesIndex == seriesCount - 1) + + // Generate bin ranges for display, applying rounding only here + var binRanges = new List>(); + for (int i = 0; i < numBins; i++) { - linePrefix += "└ "; + var start = Math.Round(seriesBinEdges[i], seriesDecimalPlaces); + var end = Math.Round(seriesBinEdges[i + 1], seriesDecimalPlaces); + binRanges.Add(Tuple.Create(start, end)); } - else if (seriesIndex == seriesCount / 2) + + // Find the maximum bin count for normalizing the bars + var maxBinCount = seriesBins.Max(); + + // Generate the distribution chart for this series + var labelWidth = 27; // Adjust as necessary + var barMaxLength = 40; // Maximum length of the bar + + var formatStr = "F" + seriesDecimalPlaces; // Format string for decimal places + + for (var i = 0; i < numBins; i++) { - linePrefix = rangeStr + " ┤ "; + var start = binRanges[i].Item1; + var end = binRanges[i].Item2; + + // Format the bin range string + var startStr = (start.ToString(formatStr) + seriesUnit).PadLeft(10); + var endStr = (end.ToString(formatStr) + seriesUnit).PadRight(10); + var rangeStr = $"{startStr} - {endStr}"; + rangeStr = rangeStr.PadLeft(labelWidth); + + var count = seriesBins[i]; + var barLength = maxBinCount > 0 ? (int)Math.Round((double)count / maxBinCount * barMaxLength) : 0; + var barChar = seriesChars[seriesLabel]; + var barColor = seriesColors[seriesLabel]; + var bar = new string(barChar, barLength); + + // Use AnsiConsole to print colored bars with counts + AnsiConsole.MarkupLine(rangeStr + " ├ " + $"[{barColor}]{bar.PadRight(barMaxLength)} ({count})[/]"); } - else + + // Display the legend + AnsiConsole.MarkupLine(" [aqua]Legend:[/]"); + if (dataSeriesDict.TryGetValue(seriesLabel, out var result)) { - linePrefix += "│ "; + // Format the width string + var seriesRangeStr = $"Width: {seriesRange.ToString(formatStr)}{seriesUnit}"; + + if (result.IsBimodal) + { + if (seriesColors.TryGetValue(seriesLabel, out var color)) + { + AnsiConsole.MarkupLine( + $" [{color}]{seriesChars[seriesLabel]}[/] : [dodgerblue1 bold]{seriesLabel}[/] {seriesRangeStr} [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + } + else + { + AnsiConsole.MarkupLine( + $" {seriesChars[seriesLabel]} : [dodgerblue1 bold]{seriesLabel}[/] {seriesRangeStr} [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + } + } + else + { + if (seriesColors.TryGetValue(seriesLabel, out var color)) + { + AnsiConsole.MarkupLine( + $" [{color}]{seriesChars[seriesLabel]}[/] : [dodgerblue1 bold]{seriesLabel}[/] {seriesRangeStr}"); + } + else + { + AnsiConsole.MarkupLine( + $" {seriesChars[seriesLabel]} : [dodgerblue1 bold]{seriesLabel}[/] {seriesRangeStr}"); + } + } } - // Use AnsiConsole to print colored bars with counts - AnsiConsole.MarkupLine(linePrefix + $"[{barColor}]{bar.PadRight(barMaxLength)} ({count})[/]"); - seriesIndex++; + AnsiConsole.WriteLine(); } } - - // Display the legend - AnsiConsole.MarkupLine(" [aqua]Legend:[/]"); - foreach (var kvp in seriesChars) + else { - if (dataSeriesDict.TryGetValue(kvp.Key, out var result)) + // Generate bin ranges for display, applying rounding only here + var binRanges = new List>(); + for (int i = 0; i < numBins; i++) + { + var start = Math.Round(binEdges[i], decimalPlaces); + var end = Math.Round(binEdges[i + 1], decimalPlaces); + binRanges.Add(Tuple.Create(start, end)); + } + + // Find the maximum bin count across all series for normalizing the bars + var maxBinCount = binsPerSeries.Values.SelectMany(k => k).Max(); + + // Generate the distribution chart + var labelWidth = 27; // Adjust as necessary + var barMaxLength = 40; // Maximum length of the bar + + var formatStr = "F" + decimalPlaces; // Format string for decimal places + + for (var i = 0; i < numBins; i++) { - if (result.IsBimodal) + var start = binRanges[i].Item1; + var end = binRanges[i].Item2; + + // Format the bin range string + var startStr = (start.ToString(formatStr) + unit).PadLeft(10); + var endStr = (end.ToString(formatStr) + unit).PadRight(10); + var rangeStr = $"{startStr} - {endStr}"; + rangeStr = rangeStr.PadLeft(labelWidth); + + var seriesCount = scaledDataSeriesDict.Keys.Count; + var seriesIndex = 0; + + foreach (var seriesLabel in scaledDataSeriesDict.Keys) { - if (seriesColors.TryGetValue(kvp.Key, out var color)) + var count = binsPerSeries[seriesLabel][i]; + var barLength = maxBinCount > 0 ? (int)Math.Round((double)count / maxBinCount * barMaxLength) : 0; + var barChar = seriesChars[seriesLabel]; + var barColor = seriesColors[seriesLabel]; + var bar = new string(barChar, barLength); + + var linePrefix = string.Empty.PadLeft(labelWidth + 1); + + if (seriesCount == 1) + { + linePrefix = rangeStr + " ├ "; + } + else if (seriesIndex == 0) + { + if (seriesCount == 2) + { + linePrefix = rangeStr + " ┌ "; + } + else + { + linePrefix += "┌ "; + } + } + else if (seriesIndex == seriesCount - 1) + { + linePrefix += "└ "; + } + else if (seriesIndex == seriesCount / 2) { - AnsiConsole.MarkupLine( - $" [{color}]{kvp.Value}[/] : [dodgerblue1 bold]{kvp.Key}[/] [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + linePrefix = rangeStr + " ┤ "; } else { - AnsiConsole.MarkupLine( - $" {kvp.Value} : [dodgerblue1 bold]{kvp.Key}[/] [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + linePrefix += "│ "; } + + // Use AnsiConsole to print colored bars with counts + AnsiConsole.MarkupLine(linePrefix + $"[{barColor}]{bar.PadRight(barMaxLength)} ({count})[/]"); + seriesIndex++; } - else + } + + // Display the legend + AnsiConsole.MarkupLine(" [aqua]Legend:[/]"); + foreach (var kvp in seriesChars) + { + if (dataSeriesDict.TryGetValue(kvp.Key, out var result)) { - if (seriesColors.TryGetValue(kvp.Key, out var color)) + // Get the scaled data for this series + var seriesData = scaledDataSeriesDict[kvp.Key]; + var seriesMinData = seriesData.Min(); + var seriesMaxData = seriesData.Max(); + var seriesRange = seriesMaxData - seriesMinData; + + // Format the width string + var seriesRangeStr = $"Width: {seriesRange.ToString(formatStr)}{unit}"; + + if (result.IsBimodal) { - AnsiConsole.MarkupLine($" [{color}]{kvp.Value}[/] : [dodgerblue1 bold]{kvp.Key}[/]"); + if (seriesColors.TryGetValue(kvp.Key, out var color)) + { + AnsiConsole.MarkupLine( + $" [{color}]{kvp.Value}[/] : [dodgerblue1 bold]{kvp.Key}[/] {seriesRangeStr} [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + } + else + { + AnsiConsole.MarkupLine( + $" {kvp.Value} : [dodgerblue1 bold]{kvp.Key}[/] {seriesRangeStr} [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + } } else { - AnsiConsole.MarkupLine($" {kvp.Value} : [dodgerblue1 bold]{kvp.Key}[/]"); + if (seriesColors.TryGetValue(kvp.Key, out var color)) + { + AnsiConsole.MarkupLine( + $" [{color}]{kvp.Value}[/] : [dodgerblue1 bold]{kvp.Key}[/] {seriesRangeStr}"); + } + else + { + AnsiConsole.MarkupLine( + $" {kvp.Value} : [dodgerblue1 bold]{kvp.Key}[/] {seriesRangeStr}"); + } } } } - } - // Display the overall range - AnsiConsole.MarkupLine($" [aqua]Range: {range.ToString(formatStr)}{unit}[/]"); - AnsiConsole.WriteLine(); + AnsiConsole.WriteLine(); + } } } \ No newline at end of file