From cf846ac27e83737acd792814333b4a2f838f3c4b Mon Sep 17 00:00:00 2001 From: Tony Redondo Date: Tue, 26 Nov 2024 22:54:46 +0100 Subject: [PATCH] Improve chart --- .../Configuration/Config.cs | 2 +- .../Exporters/ConsoleExporter.cs | 323 ++++++++++++++---- 2 files changed, 251 insertions(+), 74 deletions(-) diff --git a/src/TimeItSharp.Common/Configuration/Config.cs b/src/TimeItSharp.Common/Configuration/Config.cs index 486107e..8282998 100644 --- a/src/TimeItSharp.Common/Configuration/Config.cs +++ b/src/TimeItSharp.Common/Configuration/Config.cs @@ -95,7 +95,7 @@ public Config() 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 cb48fd9..e6f2faa 100644 --- a/src/TimeItSharp.Common/Exporters/ConsoleExporter.cs +++ b/src/TimeItSharp.Common/Exporters/ConsoleExporter.cs @@ -420,19 +420,26 @@ static void GenerateDistributionChart(Dictionary dataSer if (binIndex >= numBins) binIndex = numBins - 1; // Include the maximum in the last bin bins[binIndex]++; } - } + } + + // Check if distributions are overlapping or not + // Simplified overlapping detection + var overlappingBinsThreshold = 4; // Set your desired threshold here + var overlappingBinsCount = 0; - // Generate bin ranges for display, applying rounding only here - var binRanges = new List>(); - for (int i = 0; i < numBins; i++) + for (var 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(); @@ -457,106 +464,276 @@ static void GenerateDistributionChart(Dictionary dataSer 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(); + + // Determine unit and scale for this series based on its data + string seriesUnit; + double seriesScale; + if (seriesMaxData >= 60_000_000_000.0 / scale) + { + seriesUnit = "m"; + seriesScale = 60_000_000_000.0 / scale; + } + else if (seriesMaxData >= 1_000_000_000.0 / scale) + { + seriesUnit = "s"; + seriesScale = 1_000_000_000.0 / scale; + } + else if (seriesMaxData >= 1_000_000.0 / scale) + { + seriesUnit = "ms"; + seriesScale = 1_000_000.0 / scale; + } + else if (seriesMaxData >= 1_000.0 / scale) + { + seriesUnit = "μs"; + seriesScale = 1_000.0 / scale; + } + else + { + seriesUnit = "ns"; + seriesScale = 1.0 / scale; + } - // 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); + // Re-scale data if necessary + if (seriesScale != 1.0) + { + data = data.Select(d => d / seriesScale).ToList(); + seriesMinData = data.Min(); + seriesMaxData = data.Max(); + } - var seriesCount = scaledDataSeriesDict.Keys.Count; - var seriesIndex = 0; + // Calculate the range and bin size + 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 linePrefix = string.Empty.PadLeft(labelWidth + 1); + var seriesBinSize = seriesRange / numBins; - if (seriesCount == 1) + // Determine the number of decimal places based on binSize + int seriesDecimalPlaces = seriesBinSize >= 1 ? 1 : (int)Math.Ceiling(-Math.Log10(seriesBinSize)) + 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 += "│ "; + if (result.IsBimodal) + { + if (seriesColors.TryGetValue(seriesLabel, out var color)) + { + AnsiConsole.MarkupLine( + $" [{color}]{seriesChars[seriesLabel]}[/] : [dodgerblue1 bold]{seriesLabel}[/] [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + } + else + { + AnsiConsole.MarkupLine( + $" {seriesChars[seriesLabel]} : [dodgerblue1 bold]{seriesLabel}[/] [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + } + } + else + { + if (seriesColors.TryGetValue(seriesLabel, out var color)) + { + AnsiConsole.MarkupLine( + $" [{color}]{seriesChars[seriesLabel]}[/] : [dodgerblue1 bold]{seriesLabel}[/]"); + } + else + { + AnsiConsole.MarkupLine( + $" {seriesChars[seriesLabel]} : [dodgerblue1 bold]{seriesLabel}[/]"); + } + } } - // Use AnsiConsole to print colored bars with counts - AnsiConsole.MarkupLine(linePrefix + $"[{barColor}]{bar.PadRight(barMaxLength)} ({count})[/]"); - seriesIndex++; + // Display the range + AnsiConsole.MarkupLine($" [aqua]Range: {seriesRange.ToString(formatStr)}{seriesUnit}[/]"); + 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 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); + + var linePrefix = string.Empty.PadLeft(labelWidth + 1); + + if (seriesCount == 1) { - AnsiConsole.MarkupLine( - $" [{color}]{kvp.Value}[/] : [dodgerblue1 bold]{kvp.Key}[/] [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + linePrefix = rangeStr + " ├ "; + } + else if (seriesIndex == 0) + { + if (seriesCount == 2) + { + linePrefix = rangeStr + " ┌ "; + } + else + { + linePrefix += "┌ "; + } + } + else if (seriesIndex == seriesCount - 1) + { + linePrefix += "└ "; + } + else if (seriesIndex == seriesCount / 2) + { + 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)) + 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}[/] [yellow bold]Bimodal with peak count: {result.PeakCount}[/]"); + } + else + { + AnsiConsole.MarkupLine( + $" {kvp.Value} : [dodgerblue1 bold]{kvp.Key}[/] [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}[/]"); + } + else + { + AnsiConsole.MarkupLine($" {kvp.Value} : [dodgerblue1 bold]{kvp.Key}[/]"); + } } } } - } - // Display the overall range - AnsiConsole.MarkupLine($" [aqua]Range: {range.ToString(formatStr)}{unit}[/]"); - AnsiConsole.WriteLine(); + // Display the overall range + AnsiConsole.MarkupLine($" [aqua]Range: {range.ToString(formatStr)}{unit}[/]"); + AnsiConsole.WriteLine(); + } } } \ No newline at end of file