using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using TSLab.Script; using TSLab.Script.CanvasPane; using TSLab.Script.Handlers; using static TSLab.Script.ScriptColors; namespace GAA.ServiceLib.Graphics { /// /// Класс построения дополнительных графиков /// public static class PlotGraphs { /// /// Метод строит гистрограмму распределения параметра по частоте повторения /// /// Контекст /// Имя окна гистрограммы /// Имя панели гистограммы /// Массив значений /// Массив частот значений /// Кол-во групп распредения на графике /// Цвет гистрограммы /// Выводить или нет красную нулевую линию на гистрограмму /// Выводить или нет линию среднего значения коллекции параметров. Например, если строится распределение /// доходности по сделкам в процентах, это будет линия доходности в средней сделке (средний ПУ%) /// Выводить или нет подпись распределения параметров в процентах на гистрограмму public static void PlotDistributionHistogram(this IContext ctx, string wndName, string paneName, double[] values, int[] frequency, uint groupsNumber, Color histogramBarsColor, bool showZeroLine = true, bool showAvgValueLine = true, bool showDistrPct = true, uint yAxisPrecision = 2) { // Проверка начальных данных if (!CheckInitials()) return; // Формируем диапазоны распределения признаков var limits = new double[groupsNumber + 1]; var minValue = values[0]; var maxValue = values[^1]; var roundDigits = Math.Abs(minValue) < 1 || maxValue < 1 ? 5 : 2; var step = Math.Round((maxValue - minValue) / groupsNumber, roundDigits, MidpointRounding.AwayFromZero); limits[0] = minValue; limits[^1] = maxValue; for (var i = 1; i <= groupsNumber - 1; i++) limits[i] = Math.Round(limits[i - 1] + step, roundDigits, MidpointRounding.AwayFromZero); // Заполняем частоту признака в диапазоне var startPos = 1; var limitsQuantity = new int[limits.Length - 1]; for (var i = 1; i < limits.Length; i++) { var counter = i == 1 ? frequency[0] : 0; for (var j = startPos; j < values.Length; j++) { var cond = values[j] > limits[i - 1] && values[j] <= limits[i]; if (cond) counter += frequency[j]; if (!cond || j == values.Length - 1) { limitsQuantity[i - 1] = counter; startPos = j; break; } } } // Формируем данные для построения распределения var points = new List(); for (var i = 1; i < limits.Length; i++) { var point1 = new InteractivePointActive(limits[i - 1], 0); var point2 = new InteractivePointActive(limits[i - 1], limitsQuantity[i - 1]); var point3 = new InteractivePointActive(limits[i], limitsQuantity[i - 1]); var point4 = new InteractivePointActive(limits[i], 0); points.Add(new InteractiveObject(point1)); points.Add(new InteractiveObject(point2)); points.Add(new InteractiveObject(point3)); points.Add(new InteractiveObject(point4)); } points.Add(new InteractiveObject(new InteractivePointActive(limits[0], 0))); // Расчитываем кол-во выше/ниже/равно нулю. Формируем имя панели var belowZero = 0; var zero = 0; var aboveZero = 0; var frqSum = frequency.Sum(); for (var i = 0; i < values.Length; i++) { var frq = frequency[i]; if (values[i] < 0) belowZero += frq; else if (values[i] == 0) zero += frq; else aboveZero += frq; } var distrData = $"BelowZero: {Math.Round(belowZero * 100d / frqSum, 2, MidpointRounding.AwayFromZero)}%," + $" Zero: {Math.Round(zero * 100d / frqSum, 2, MidpointRounding.AwayFromZero)}%," + $" AboveZero: {Math.Round(aboveZero * 100d / frqSum, 2, MidpointRounding.AwayFromZero)}%"; // Формируем окно, графическую панель, задаем точность var distrWnd = ctx.AddWindow(wndName, wndName); var canvPane = distrWnd.CreateCanvasPane(paneName, $"{paneName}{(showDistrPct ? $". {distrData}" : "")}"); canvPane.XAxisPrecision = 2; canvPane.YAxisPrecision = (int)yAxisPrecision; // Добавляем распределение на график, отрисовываем нулевую линию _ = canvPane.AddList("Distribution", "Distribution", new InteractiveSeries() { ControlPoints = new ReadOnlyCollection(points) }, ListStyles.SPLINE, histogramBarsColor, LineStyles.SOLID, PaneSides.RIGHT, PointParameters.Empty, PointParameters.Empty, PointParameters.Empty); // Формируем и добавляем на график нулевую линию if (showZeroLine) { var zeroLine = new List(); var pointStart = new InteractivePointActive(0, 0); var pointEnd = new InteractivePointActive(0, limitsQuantity.Max()); zeroLine.Add(new InteractiveObject(pointStart)); zeroLine.Add(new InteractiveObject(pointEnd)); var serZeroLine = canvPane.AddList("ZeroLine", "ZeroLine", new InteractiveSeries() { ControlPoints = new ReadOnlyCollection(zeroLine) }, ListStyles.SPLINE, DarkRed, LineStyles.SOLID, PaneSides.RIGHT, PointParameters.Empty, PointParameters.Empty, PointParameters.Empty); serZeroLine.Thickness = 2; } // Формируем и добавляем линию среднего значения if (showAvgValueLine) { // Находим среднее, перемножаем все значения на их частоты // и делим на сумму частот var avgValue = 0d; for (var i = 0; i < values.Length; i++) avgValue += values[i] * frequency[i]; avgValue /= frequency.Sum(); var avgValueLine = new List(); var pointStart = new InteractivePointActive(avgValue, 0); var pointEnd = new InteractivePointActive(avgValue, limitsQuantity.Max()); avgValueLine.Add(new InteractiveObject(pointStart)); avgValueLine.Add(new InteractiveObject(pointEnd)); var serAvgValueLine = canvPane.AddList("AvgValueLine", "AvgValueLine", new InteractiveSeries() { ControlPoints = new ReadOnlyCollection(avgValueLine) }, ListStyles.SPLINE, DarkGreen, LineStyles.DOT, PaneSides.RIGHT, PointParameters.Empty, PointParameters.Empty, PointParameters.Empty); serAvgValueLine.Thickness = 2; } // Выравниваем графические данные по центру области построения var borderOffset = 5 / 100d; var xStep = (limits.Max() - limits.Min()) * borderOffset; var yStep = (limitsQuantity.Max() - limitsQuantity.Min()) * borderOffset; canvPane.Border2 = new Border(limits.Min() - xStep, limitsQuantity.Max() + (yStep * 2), limits.Max() + xStep, limitsQuantity.Min() - yStep); canvPane.FeetToBorder2ByDefault = true; canvPane.FeetToBorder2 = true; // Метод осуществляет контроль начальных параметров bool CheckInitials() { if (values.Length <= 1 || frequency.Length <= 1) { ctx.Log($"Окно: {wndName}. Недостаточно данных для формирования распределения, необходимо минимум 2 значения" + $" в массивах \"values\" и \"frequency\""); return false; } if (values.Length != frequency.Length) { ctx.Log($"Окно: {wndName}. Количество данных в массивах \"values\" и \"frequency\" не совпадает. Построение частотного распределения невозможно"); return false; } if (groupsNumber < 1) { ctx.Log($"Окно: {wndName}. Количество групп распределения не может быть меньше одной. Построение частотного распределения невозможно"); return false; } if (groupsNumber > 200) { ctx.Log($"Окно: {wndName}. Количество групп распределения не может быть более 200. Построение частотного распределения невозможно"); return false; } return true; } } /// /// Метод строит диаграмму распределения параметра в виде точек на двумерной плоскости /// /// Контекст /// Имя окна диаграммы /// Имя панели диаграммы /// Значения по оси Х /// Значение по оси Y /// Цвет точки на диаграмме /// Размер точки на диаграмме /// При указании имени на диаграмму около каждой точки будет выведена подпись /// При установке данной опциий около каждой точки будет указан ее порядковый номер в массиве значений начиная с 1 /// При установке данной опциий около каждой точки будет указано ее значение X-Y public static void PlotDistributionPointChart(this IContext ctx, string wndName, string paneName, double[] xValues, double[] yValues, Color pointColor, uint pointSize = 1, string pointName = "", bool addPointNumbers = false, bool addPointValues = false) { // Проверка начальных данных if (!CheckInitials()) return; // Формируем окно, панель, задаем округление var pointWnd = ctx.AddWindow(wndName, wndName); var canvPane = pointWnd.CreateCanvasPane(paneName, paneName); canvPane.XAxisPrecision = 2; canvPane.YAxisPrecision = 2; var points = new List(); for (var i = 1; i < Math.Min(xValues.Length, 25000); i++) { var label = $"{pointName}{(addPointNumbers ? $"_{i}" : "")}{(addPointValues ? $"_{xValues[i]},{yValues[i]}" : "")}"; if (label != "" && label[0] == '_') label = label[1..]; var point = new InteractivePointActive(xValues[i], yValues[i]) { Size = pointSize, Label = label }; points.Add(new InteractiveObject(point)); } var pointCollection = new ReadOnlyCollection(points); var serie = new InteractiveSeries() { ControlPoints = pointCollection }; var pointParam = new PointParameters(pointColor, DragableMode.None, Geometries.Circle, true); var pointsLine = canvPane.AddList($"Points", $"Points", serie, ListStyles.POINT, pointColor, LineStyles.SOLID, PaneSides.RIGHT, pointParam, pointParam, pointParam); pointsLine.Opacity = 255; // Выравниваем графические данные по центру области построения var borderOffset = 10 / 100d; var xStep = (xValues.Max() - xValues.Min()) * borderOffset; var yStep = (yValues.Max() - yValues.Min()) * borderOffset; canvPane.Border2 = new Border(xValues.Min() - xStep, yValues.Max() + yStep, xValues.Max() + xStep, yValues.Min() - yStep); canvPane.FeetToBorder2ByDefault = true; canvPane.FeetToBorder2 = true; // Метод осуществляет контроль начальных параметров bool CheckInitials() { if (xValues.Length != yValues.Length) { ctx.Log($"Окно: {wndName}. Количество данных в массивах \"xValues\" и \"yValues\" не совпадает. Построение диаграммы невозможно"); return false; } if (pointSize < 1) { ctx.Log($"Окно: {wndName}. Размер точки не может быть меньше единицы. Построение диаграммы невозможно"); return false; } return true; } } } }