feat: add FundSkiaChartRenderer (SkiaSharp price line chart)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<FundChartPoint> 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<FundChartPoint> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user