This repository has been archived on 2024-12-22. You can view files and clone it, but cannot push or open issues or pull requests.
elliebot/src/EllieBot/Modules/Searches/Crypto/Drawing/ImagesharpStockChartDrawingService.cs
2024-09-04 22:02:50 +12:00

200 lines
No EOL
6.5 KiB
C#

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.Runtime.CompilerServices;
using Color = SixLabors.ImageSharp.Color;
namespace EllieBot.Modules.Searches;
public sealed class ImagesharpStockChartDrawingService : IStockChartDrawingService, IEService
{
private const int WIDTH = 300;
private const int HEIGHT = 100;
private const decimal MAX_HEIGHT = HEIGHT * 0.8m;
private static readonly Rgba32 _backgroundColor = Rgba32.ParseHex("17181E");
private static readonly Rgba32 _lineGuideColor = Rgba32.ParseHex("212125");
private static readonly Rgba32 _sparklineColor = Rgba32.ParseHex("2961FC");
private static readonly Rgba32 _greenBrush = Rgba32.ParseHex("26A69A");
private static readonly Rgba32 _redBrush = Rgba32.ParseHex("EF5350");
private static float GetNormalizedPoint(decimal max, decimal point, decimal range)
=> (float)((MAX_HEIGHT * ((max - point) / range)) + HeightOffset());
private PointF[] GetSparklinePointsInternal(IReadOnlyCollection<CandleData> series)
{
var candleStep = WIDTH / (series.Count + 1);
var max = series.Max(static x => x.High);
var min = series.Min(static x => x.Low);
var range = max - min;
var points = new PointF[series.Count];
var i = 0;
foreach (var candle in series)
{
var x = candleStep * (i + 1);
var y = GetNormalizedPoint(max, candle.Close, range);
points[i++] = new(x, y);
}
return points;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static decimal HeightOffset()
=> (HEIGHT - MAX_HEIGHT) / 2m;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Image<Rgba32> CreateCanvasInternal()
=> new Image<Rgba32>(WIDTH, HEIGHT, _backgroundColor);
private CandleDrawingData[] GetChartDrawingDataInternal(IReadOnlyCollection<CandleData> series)
{
var candleMargin = 2;
var candleStep = (WIDTH - (candleMargin * series.Count)) / (series.Count + 1);
var max = series.Max(static x => x.High);
var min = series.Min(static x => x.Low);
var range = max - min;
var drawData = new CandleDrawingData[series.Count];
var candleWidth = candleStep;
var i = 0;
foreach (var candle in series)
{
var offsetX = (i - 1) * candleMargin;
var x = (candleStep * (i + 1)) + offsetX;
var yOpen = GetNormalizedPoint(max, candle.Open, range);
var yClose = GetNormalizedPoint(max, candle.Close, range);
var y = candle.Open > candle.Close
? yOpen
: yClose;
var sizeH = Math.Abs(yOpen - yClose);
var high = GetNormalizedPoint(max, candle.High, range);
var low = GetNormalizedPoint(max, candle.Low, range);
drawData[i] = new(candle.Open < candle.Close,
new(x, y, candleWidth, sizeH),
new(x + (candleStep / 2), high),
new(x + (candleStep / 2), low));
++i;
}
return drawData;
}
private void DrawChartData(Image<Rgba32> image, CandleDrawingData[] drawData)
=> image.Mutate(ctx =>
{
foreach (var data in drawData)
ctx.DrawLine(data.IsGreen
? _greenBrush
: _redBrush,
1,
data.High,
data.Low);
foreach (var data in drawData)
ctx.Fill(data.IsGreen
? _greenBrush
: _redBrush,
data.BodyRect);
});
private void DrawLineGuides(Image<Rgba32> image, IReadOnlyCollection<CandleData> series)
{
var max = series.Max(x => x.High);
var min = series.Min(x => x.Low);
var step = (max - min) / 5;
var lines = new float[6];
for (var i = 0; i < 6; i++)
{
var y = GetNormalizedPoint(max, min + (step * i), max - min);
lines[i] = y;
}
image.Mutate(ctx =>
{
// draw guides
foreach (var y in lines)
ctx.DrawLine(_lineGuideColor, 1, new PointF(0, y), new PointF(WIDTH, y));
// // draw min and max price on the chart
// ctx.DrawText(min.ToString(CultureInfo.InvariantCulture),
// SystemFonts.CreateFont("Arial", 5),
// Color.White,
// new PointF(0, (float)HeightOffset() - 5)
// );
//
// ctx.DrawText(max.ToString("N1", CultureInfo.InvariantCulture),
// SystemFonts.CreateFont("Arial", 5),
// Color.White,
// new PointF(0, HEIGHT - (float)HeightOffset())
// );
});
}
public Task<ImageData?> GenerateSparklineAsync(IReadOnlyCollection<CandleData> series)
{
if (series.Count == 0)
return Task.FromResult<ImageData?>(default);
using var image = CreateCanvasInternal();
var points = GetSparklinePointsInternal(series);
image.Mutate(ctx =>
{
ctx.DrawLine(_sparklineColor, 2, points);
});
return Task.FromResult<ImageData?>(new("png", image.ToStream()));
}
public Task<ImageData?> GenerateCombinedChartAsync(IReadOnlyCollection<CandleData> series)
{
if (series.Count == 0)
return Task.FromResult<ImageData?>(default);
using var image = CreateCanvasInternal();
DrawLineGuides(image, series);
var chartData = GetChartDrawingDataInternal(series);
DrawChartData(image, chartData);
var points = GetSparklinePointsInternal(series);
image.Mutate(ctx =>
{
ctx.DrawLine(Color.ParseHex("00FFFFAA"), 1, points);
});
return Task.FromResult<ImageData?>(new("png", image.ToStream()));
}
public Task<ImageData?> GenerateCandleChartAsync(IReadOnlyCollection<CandleData> series)
{
if (series.Count == 0)
return Task.FromResult<ImageData?>(default);
using var image = CreateCanvasInternal();
DrawLineGuides(image, series);
var drawData = GetChartDrawingDataInternal(series);
DrawChartData(image, drawData);
return Task.FromResult<ImageData?>(new("png", image.ToStream()));
}
}