diff --git a/CertReports.Syncfusion/Services/Implementations/FundSkiaChartRenderer.cs b/CertReports.Syncfusion/Services/Implementations/FundSkiaChartRenderer.cs new file mode 100644 index 0000000..57d0f84 --- /dev/null +++ b/CertReports.Syncfusion/Services/Implementations/FundSkiaChartRenderer.cs @@ -0,0 +1,188 @@ +using CertReports.Syncfusion.Models; +using SkiaSharp; + +namespace CertReports.Syncfusion.Services.Implementations; + +public static class FundSkiaChartRenderer +{ + private const int DefaultWidth = 1100; + private const int DefaultHeight = 700; + private const float MarginLeft = 70f; + private const float MarginRight = 40f; + private const float MarginTop = 55f; + private const float MarginBottom = 55f; + + public static byte[] Render(List points, string instrumentName, + int width = DefaultWidth, int height = DefaultHeight, bool jpeg = false) + { + using var surface = SKSurface.Create(new SKImageInfo(width, height)); + var canvas = surface.Canvas; + canvas.Clear(SKColors.White); + + if (points.Count < 2) + DrawNoDataMessage(canvas, width, height, instrumentName); + else + DrawChart(canvas, points, instrumentName, width, height); + + using var image = surface.Snapshot(); + using var data = jpeg + ? image.Encode(SKEncodedImageFormat.Jpeg, 90) + : image.Encode(SKEncodedImageFormat.Png, 100); + return data.ToArray(); + } + + private static void DrawChart(SKCanvas canvas, List points, + string title, int width, int height) + { + float plotX = MarginLeft; + float plotY = MarginTop; + float plotW = width - MarginLeft - MarginRight; + float plotH = height - MarginTop - MarginBottom; + + decimal minPrice = points.Min(p => p.Close); + decimal maxPrice = points.Max(p => p.Close); + decimal priceRange = maxPrice - minPrice; + if (priceRange == 0) priceRange = 1; + decimal pad = priceRange * 0.08m; + decimal yMin = minPrice - pad; + decimal yMax = maxPrice + pad; + + var minDate = points.First().Date; + var maxDate = points.Last().Date; + double totalDays = (maxDate - minDate).TotalDays; + + // Plot area background + using var bgPaint = new SKPaint { Color = new SKColor(250, 250, 255), Style = SKPaintStyle.Fill }; + canvas.DrawRect(plotX, plotY, plotW, plotH, bgPaint); + + DrawGrid(canvas, plotX, plotY, plotW, plotH, yMin, yMax, minDate, maxDate, totalDays); + + // Price line + using var linePaint = new SKPaint + { + Color = SKColors.Black, + StrokeWidth = 2f, + IsAntialias = true, + Style = SKPaintStyle.Stroke + }; + using var path = new SKPath(); + bool first = true; + foreach (var pt in points) + { + float px = DateToX(pt.Date, minDate, maxDate, plotX, plotW); + float py = PriceToY(pt.Close, yMin, yMax, plotY, plotH); + if (first) { path.MoveTo(px, py); first = false; } + else path.LineTo(px, py); + } + canvas.DrawPath(path, linePaint); + + // End label (last price) + var last = points.Last(); + float lastX = DateToX(last.Date, minDate, maxDate, plotX, plotW); + float lastY = PriceToY(last.Close, yMin, yMax, plotY, plotH); + var labelFont = new SKFont(SKTypeface.Default, 11f); + using var labelPaint = new SKPaint { Color = SKColors.Black, IsAntialias = true }; + canvas.DrawText($"{last.Close:F2}", lastX + 4f, lastY + 4f, + SKTextAlign.Left, labelFont, labelPaint); + + // Chart title + var titleFont = new SKFont(SKTypeface.Default, 14f) { Embolden = true }; + using var titlePaint = new SKPaint + { + Color = new SKColor(21, 101, 192), + IsAntialias = true + }; + canvas.DrawText(title, (float)width / 2, MarginTop - 10f, + SKTextAlign.Center, titleFont, titlePaint); + + // Plot border + using var borderPaint = new SKPaint + { + Color = new SKColor(200, 200, 200), + StrokeWidth = 0.8f, + Style = SKPaintStyle.Stroke + }; + canvas.DrawRect(plotX, plotY, plotW, plotH, borderPaint); + } + + private static void DrawGrid(SKCanvas canvas, + float plotX, float plotY, float plotW, float plotH, + decimal yMin, decimal yMax, + DateTime minDate, DateTime maxDate, double totalDays) + { + using var gridPaint = new SKPaint + { + Color = new SKColor(220, 220, 220), + StrokeWidth = 0.5f, + Style = SKPaintStyle.Stroke + }; + using var axisPaint = new SKPaint + { + Color = new SKColor(100, 100, 100), + StrokeWidth = 0.8f, + Style = SKPaintStyle.Stroke + }; + using var axisLabelPaint = new SKPaint + { + Color = new SKColor(100, 100, 100), + IsAntialias = true + }; + var axisLabelFont = new SKFont(SKTypeface.Default, 10f); + + // Y grid (5 horizontal lines) + for (int i = 0; i <= 5; i++) + { + decimal price = yMin + (yMax - yMin) * i / 5; + float py = PriceToY(price, yMin, yMax, plotY, plotH); + canvas.DrawLine(plotX, py, plotX + plotW, py, i == 0 ? axisPaint : gridPaint); + canvas.DrawText($"{price:F2}", plotX - 5f, py + 4f, + SKTextAlign.Right, axisLabelFont, axisLabelPaint); + } + + // X grid (adaptive monthly intervals) + int monthInterval = totalDays > 365 * 3 ? 12 : + totalDays > 365 ? 6 : + totalDays > 90 ? 3 : 1; + + var current = new DateTime(minDate.Year, minDate.Month, 1).AddMonths(1); + while (current <= maxDate) + { + if (current.Month % monthInterval == 0) + { + float px = DateToX(current, minDate, maxDate, plotX, plotW); + canvas.DrawLine(px, plotY, px, plotY + plotH, gridPaint); + var lbl = monthInterval >= 12 + ? current.Year.ToString() + : current.ToString("MMM yy"); + canvas.DrawText(lbl, px, plotY + plotH + 14f, + SKTextAlign.Center, axisLabelFont, axisLabelPaint); + } + current = current.AddMonths(1); + } + } + + private static void DrawNoDataMessage(SKCanvas canvas, int width, int height, string title) + { + var font = new SKFont(SKTypeface.Default, 14f); + using var paint = new SKPaint { Color = new SKColor(150, 150, 150), IsAntialias = true }; + canvas.DrawText($"{title} — Dati insufficienti", + (float)width / 2, (float)height / 2, SKTextAlign.Center, font, paint); + } + + private static float DateToX(DateTime date, DateTime minDate, DateTime maxDate, + float plotX, float plotW) + { + double totalDays = (maxDate - minDate).TotalDays; + if (totalDays == 0) return plotX; + double ratio = (date - minDate).TotalDays / totalDays; + return plotX + (float)(ratio * plotW); + } + + private static float PriceToY(decimal price, decimal yMin, decimal yMax, + float plotY, float plotH) + { + if (yMax == yMin) return plotY + plotH / 2; + double ratio = (double)((price - yMin) / (yMax - yMin)); + return plotY + plotH - (float)(ratio * plotH); + } +}