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;
}
}
}
}