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