Skip to content

Commit

Permalink
Merge pull request #4 from japsuu/dev
Browse files Browse the repository at this point in the history
Add one-month graph drawing
  • Loading branch information
japsuu authored Feb 15, 2024
2 parents 34f4230 + 790730b commit 9fcc9f1
Show file tree
Hide file tree
Showing 13 changed files with 286 additions and 97 deletions.
205 changes: 160 additions & 45 deletions DiscordClient/Graphing/GraphDrawer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,111 @@
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Path = System.IO.Path;

namespace DiscordClient.Graphing;

public static class GraphDrawer
{
private enum GraphTimePeriod
{
DAY,
MONTH
}

private static readonly string GraphBackgroundPath;
private static readonly Pen GraphingPenLow;
private static readonly Pen GraphingPenHigh;
private static readonly Pen Last6HPen;
private static readonly Pen HighlightingPen;
private static readonly Font Font;


static GraphDrawer()
{
const string fontPath = @"assets\fonts\runescape_uf.ttf";
const string graphBackgroundPath = @"assets\textures\graph_background.png";

string executingAssemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty;
GraphBackgroundPath = Path.Combine(executingAssemblyPath, graphBackgroundPath);
string fontPath1 = Path.Combine(executingAssemblyPath, fontPath);
string fPath = Path.Combine(executingAssemblyPath, fontPath);

FontCollection fontCollection = new();
FontFamily family = fontCollection.Add(fontPath1);
FontFamily family = fontCollection.Add(fPath);
Font = new Font(family, 14, FontStyle.Regular);

GraphingPenLow = new SolidPen(new SolidBrush(Color.Chartreuse), 1f);
GraphingPenHigh = new SolidPen(new SolidBrush(Color.Orange), 1f);
Last6HPen = new SolidPen(new SolidBrush(Color.Brown), 3f);
HighlightingPen = new SolidPen(new SolidBrush(Color.CadetBlue), 3f);
}


/// <summary>
/// Draws a price graph from the price history of an item.
/// </summary>
/// <param name="history5MinIntervals">Item price history, in 5min intervals. Should contain at least 288x 5min data-points = 24h.</param>
public static async Task<MemoryStream> DrawGraph(ItemPriceHistory history5MinIntervals)
/// <param name="history6HourIntervals">Item price history, in 6h intervals. May be null if not available.</param>
/// <param name="latestBuy">Latest buy price.</param>
/// <param name="latestSell">Latest sell price.</param>
public static async Task<MemoryStream> DrawGraph(
ItemPriceHistory history5MinIntervals,
ItemPriceHistory? history6HourIntervals,
int latestBuy,
int latestSell)
{
if (history5MinIntervals.Data.Count < 288)
const int graphPointsPerDay = 288; // Assuming 5min intervals.

if (history5MinIntervals.Data.Count < graphPointsPerDay)
throw new ArgumentException("The price history must contain at least 288x 5min data-points = 24h.", nameof(history5MinIntervals));
List<ItemPriceHistoryEntry> dataPoints = history5MinIntervals.Data.TakeLast(288).ToList();

List<ItemPriceHistoryEntry> dataPointsDay = history5MinIntervals.Data.TakeLast(graphPointsPerDay).ToList();
dataPointsDay.Add(
new ItemPriceHistoryEntry
{
AvgHighPrice = latestBuy,
AvgLowPrice = latestSell
});

Image dayGraph = await CreateGraph(dataPointsDay, GraphTimePeriod.DAY);
Image? monthGraph = null;

if (history6HourIntervals != null)
{
List<ItemPriceHistoryEntry> dataPointsMonth = history6HourIntervals.Data.TakeLast(120).ToList();
dataPointsMonth.Add(
new ItemPriceHistoryEntry
{
AvgHighPrice = latestBuy,
AvgLowPrice = latestSell
});

monthGraph = await CreateGraph(dataPointsMonth, GraphTimePeriod.MONTH);
}

int width = Math.Max(dayGraph.Width, monthGraph?.Width ?? 0);
int height = dayGraph.Height + (monthGraph?.Height ?? 0);
Image combinedImage = new Image<Rgba32>(width, height);
combinedImage.Mutate(ctx => ctx.DrawImage(dayGraph, new Point(0, 0), 1f));
if (monthGraph != null)
combinedImage.Mutate(ctx => ctx.DrawImage(monthGraph, new Point(0, dayGraph.Height), 1f));

MemoryStream ms = new();
await combinedImage.SaveAsync(ms, new PngEncoder());
ms.Position = 0;

combinedImage.Dispose();

return ms;
}


private static async Task<Image> CreateGraph(List<ItemPriceHistoryEntry> dataPoints, GraphTimePeriod timePeriod)
{
const int graphPadding = 15;

// 460x80 pixels.
using Image img = await Image.LoadAsync(GraphBackgroundPath);
Image img = await Image.LoadAsync(GraphBackgroundPath);
int imageWidth = img.Width;
int imageHeight = img.Height;

Expand All @@ -62,73 +122,128 @@ public static async Task<MemoryStream> DrawGraph(ItemPriceHistory history5MinInt
continue;
if ((int)entry.LowestPrice < minValue)
minValue = (int)entry.LowestPrice;

if (entry.HighestPrice == null)
continue;
if ((int)entry.HighestPrice > maxValue)
maxValue = (int)entry.HighestPrice;
}

// Determine required values.
float periodHighlightStartPoint = imageWidth - (imageWidth - graphPadding) / 4f;
int periodCount;
int periodCountForStartPoint;
string periodText;
string lastPeriodText;
switch (timePeriod)
{
case GraphTimePeriod.DAY:
periodCount = 288;
periodCountForStartPoint = 72;
periodText = "last 24h";
lastPeriodText = "last 6h ->";
break;
case GraphTimePeriod.MONTH:
periodCount = 120;
periodCountForStartPoint = 4;
periodText = "last 30d";
lastPeriodText = "last day ->";
break;
default:
throw new ArgumentOutOfRangeException(nameof(timePeriod), timePeriod, null);
}

// Construct the graph points.
List<PointF> graphPointsLow = new();
List<PointF> graphPointsHigh = new();

float last6HStartPoint = imageWidth - (imageWidth - 15) / 4f;
for (int i = 0; i < dataPoints.Count; i++)
{
if (dataPoints[i].LowestPrice != null)
if (dataPoints[i].AvgLowPrice != null)
{
PointF point = NormalizedPointFromValues(i, (int)dataPoints[i].LowestPrice!, imageWidth, imageHeight, 15, 15, dataPoints.Count, minValue, maxValue);
PointF point = NormalizedPointFromValues(
i,
(int)dataPoints[i].AvgLowPrice!,
imageWidth,
imageHeight,
graphPadding,
graphPadding,
dataPoints.Count,
minValue,
maxValue);

if (i == 288 - 72)
last6HStartPoint = point.X;
if (i == periodCount - periodCountForStartPoint)
periodHighlightStartPoint = point.X;

graphPointsLow.Add(point);
}
if (dataPoints[i].HighestPrice != null)

if (dataPoints[i].AvgHighPrice != null)
{
PointF point = NormalizedPointFromValues(i, (int)dataPoints[i].HighestPrice!, imageWidth, imageHeight, 15, 15, dataPoints.Count, minValue, maxValue);
PointF point = NormalizedPointFromValues(
i,
(int)dataPoints[i].AvgHighPrice!,
imageWidth,
imageHeight,
graphPadding,
graphPadding,
dataPoints.Count,
minValue,
maxValue);

if (i == periodCount - periodCountForStartPoint)
periodHighlightStartPoint = point.X;

if (i == 288 - 72)
last6HStartPoint = point.X;

graphPointsHigh.Add(point);
}
}


// Construct the graph paths.
PathBuilder graphPathLow = new();
PathBuilder graphPathHigh = new();
graphPathLow.AddLines(graphPointsLow.ToArray());
graphPathHigh.AddLines(graphPointsHigh.ToArray());

// Draw the graphs.
img.Mutate(ctx => ctx.Draw(GraphingPenLow, graphPathLow.Build()));
img.Mutate(ctx => ctx.Draw(GraphingPenHigh, graphPathHigh.Build()));

// Construct points for the 6h helper line.
List<PointF> last6HPoints = new();
int targetHeight6H = imageHeight - 15;
int latestPoint = imageWidth - 15;

last6HPoints.Add(new PointF(last6HStartPoint, targetHeight6H));
last6HPoints.Add(new PointF(latestPoint, targetHeight6H));

// Draw the 6h helper line
// Construct points for the helper line.
List<PointF> highlightedPeriodPoints = new();
int targetHeightPeriod = imageHeight - graphPadding;
int latestPoint = imageWidth - graphPadding;

// Add the last period points.
highlightedPeriodPoints.Add(new PointF(periodHighlightStartPoint, targetHeightPeriod));
highlightedPeriodPoints.Add(new PointF(latestPoint, targetHeightPeriod));

// Draw the helper line
PathBuilder helperPath = new();
helperPath.AddLines(last6HPoints.ToArray());
img.Mutate(ctx => ctx.Draw(Last6HPen, helperPath.Build()));

// Draw the "last 6h ->" text.
img.Mutate(ctx => ctx.DrawText("last 6h ->", Font, Color.Wheat, new PointF(last6HStartPoint - 60, imageHeight - 20)));
helperPath.AddLines(highlightedPeriodPoints.ToArray());
img.Mutate(ctx => ctx.Draw(HighlightingPen, helperPath.Build()));

MemoryStream ms = new();
await img.SaveAsync(ms, new PngEncoder());
ms.Position = 0;
return ms;
// Draw the "last period ->" text.
img.Mutate(ctx => ctx.DrawText(lastPeriodText, Font, Color.Wheat, new PointF(periodHighlightStartPoint - 60, imageHeight - graphPadding - 5)));

// Add the "period" text.
img.Mutate(ctx => ctx.DrawText(periodText, Font, Color.Wheat, new PointF(10, 5)));

return img;
}


private static PointF NormalizedPointFromValues(int pointIndex, int pointValue, int imageWidth, int imageHeight, int widthPadding, int heightPadding, int pointsCount, int lowestValue, int highestValue)
/// <summary>
/// Normalizes a data point to a point on the graph.
/// </summary>
private static PointF NormalizedPointFromValues(
int pointIndex,
int pointValue,
int imageWidth,
int imageHeight,
int widthPadding,
int heightPadding,
int pointsCount,
int lowestValue,
int highestValue)
{
float normalizedX = pointIndex * (imageWidth - widthPadding - widthPadding) / (float)(pointsCount - 1) + widthPadding;
float normalizedY = (pointValue - lowestValue) * (imageHeight - heightPadding - heightPadding) / (float)(highestValue - lowestValue) + heightPadding;
Expand Down
3 changes: 2 additions & 1 deletion DiscordClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,15 @@ private static async void OnDumpsUpdated()
foreach (SocketTextChannel channel in channels)
{
// Get the graph image.
MemoryStream memStream = await GraphDrawer.DrawGraph(dump.PriceHistory);
MemoryStream memStream = await GraphDrawer.DrawGraph(dump.PriceHistory5Min, dump.PriceHistory6Hour, dump.InstaBuyPrice, dump.InstaSellPrice);
FileAttachment graphAttachment = new(memStream, "graph.png");
RestUserMessage msg = await channel.SendFileAsync(graphAttachment);
string graphUrl = msg.Attachments.First().Url;

Embed embed = DumpEmbedBuilder.BuildEmbed(dump, graphUrl);
await channel.SendMessageAsync(embed: embed);
await msg.DeleteAsync();
await Task.Delay(2000);
}
}
catch (Exception e)
Expand Down
5 changes: 2 additions & 3 deletions OsrsFlipper/Api/OsrsApiController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics;
using OsrsFlipper.Data.Mapping;
using OsrsFlipper.Data.Mapping;
using OsrsFlipper.Data.Price.Average;
using OsrsFlipper.Data.Price.Latest;
using OsrsFlipper.Data.TimeSeries;
Expand Down Expand Up @@ -103,7 +102,7 @@ public async Task<ItemMapping> GetItemMapping()
}


public async Task<ItemPriceHistory?> GetPriceHistory(ItemData item, TimeSeriesApi.TimeSeriesTimeStep timestep)
public async Task<ItemPriceHistory?> GetPriceHistory(ItemData item, TimeSeriesTimeStep timestep)
{
return await _timeSeriesApi.GetPriceHistory(_client, item, timestep);
}
Expand Down
7 changes: 0 additions & 7 deletions OsrsFlipper/Api/TimeSeriesApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@ namespace OsrsFlipper.Api;

public class TimeSeriesApi : OsrsApi<ItemPriceHistory>
{
public enum TimeSeriesTimeStep
{
FiveMinutes,
Hour,
SixHours,
Day
}
private readonly RestRequest _request;


Expand Down
9 changes: 9 additions & 0 deletions OsrsFlipper/Api/TimeSeriesTimeStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace OsrsFlipper.Api;

public enum TimeSeriesTimeStep
{
FiveMinutes,
Hour,
SixHours,
Day
}
6 changes: 6 additions & 0 deletions OsrsFlipper/Caching/CacheEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public class CacheEntry
public readonly AveragedPriceData Price24HourAverage = new();


/// <summary>
/// Potential maximum amount that can be bought in 4 hours.
/// </summary>
public int MaxBuyAmount => Math.Min(Item.GeBuyLimit, Price24HourAverage.TotalVolume);


public CacheEntry(ItemData item)
{
Item = item;
Expand Down
14 changes: 14 additions & 0 deletions OsrsFlipper/Filtering/FilterCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,18 @@ public bool PassesFlipTest(CacheEntry itemData, ItemPriceHistory history)

return true;
}


public void DebugFilters()
{
foreach (PruneFilter filter in _pruneFilters)
{
Logger.Verbose($"Prune filter {filter.GetType().Name} block count: {filter.ItemsFailed} / {filter.ItemsChecked}");
}

foreach (FlipFilter filter in _flipFilters)
{
Logger.Verbose($"Flip filter {filter.GetType().Name} block count: {filter.ItemsFailed} / {filter.ItemsChecked}");
}
}
}
Loading

0 comments on commit 9fcc9f1

Please sign in to comment.