Various things. Added a tangent line system, allows for different graph parts, and more caching stuff.

This commit is contained in:
That_One_Nerd 2024-02-29 10:44:28 -05:00
parent 21c498f445
commit e31e6bfdb6
11 changed files with 294 additions and 179 deletions

View File

@ -1,5 +1,7 @@
using Graphing.Extensions; using Graphing.Extensions;
using Graphing.Graphables; using Graphing.Graphables;
using Graphing.Parts;
using System.Drawing.Drawing2D;
using System.Text; using System.Text;
namespace Graphing.Forms; namespace Graphing.Forms;
@ -145,6 +147,7 @@ public partial class GraphForm : Form
protected override void OnPaint(PaintEventArgs e) protected override void OnPaint(PaintEventArgs e)
{ {
Graphics g = e.Graphics; Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality;
Brush background = new SolidBrush(Color.White); Brush background = new SolidBrush(Color.White);
g.FillRectangle(background, e.ClipRectangle); g.FillRectangle(background, e.ClipRectangle);
@ -154,19 +157,9 @@ public partial class GraphForm : Form
// Draw the actual graphs. // Draw the actual graphs.
for (int i = 0; i < ables.Count; i++) for (int i = 0; i < ables.Count; i++)
{ {
IEnumerable<Line2d> lines = ables[i].GetItemsToRender(this); IEnumerable<IGraphPart> lines = ables[i].GetItemsToRender(this);
Brush graphBrush = new SolidBrush(ables[i].Color); Brush graphBrush = new SolidBrush(ables[i].Color);
Pen penBrush = new(graphBrush, 3); foreach (IGraphPart gp in lines) gp.Render(this, g, graphBrush);
foreach (Line2d l in lines)
{
if (!double.IsNormal(l.a.x) || !double.IsNormal(l.a.y) ||
!double.IsNormal(l.b.x) || !double.IsNormal(l.b.y)) continue;
Int2 start = GraphSpaceToScreenSpace(l.a),
end = GraphSpaceToScreenSpace(l.b);
g.DrawLine(penBrush, start, end);
}
} }
base.OnPaint(e); base.OnPaint(e);
@ -375,24 +368,17 @@ public partial class GraphForm : Form
long total = 0; long total = 0;
foreach (Graphable able in ables) foreach (Graphable able in ables)
{ {
if (able is Equation equ) long size = able.GetCacheBytes();
{
long size = equ.GetCacheBytes();
message.AppendLine($"{able.Name}: {size.FormatAsBytes()}"); message.AppendLine($"{able.Name}: {size.FormatAsBytes()}");
total += size; total += size;
} }
}
message.AppendLine($"\nTotal: {total.FormatAsBytes()}\n\nClick \"No\" to erase caches."); message.AppendLine($"\nTotal: {total.FormatAsBytes()}\n\nErase cache?");
DialogResult result = MessageBox.Show(message.ToString(), "Graph Caches", MessageBoxButtons.YesNo, MessageBoxIcon.Information); DialogResult result = MessageBox.Show(message.ToString(), "Graph Caches", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
if (result == DialogResult.No) if (result == DialogResult.Yes)
{ {
foreach (Graphable able in ables) foreach (Graphable able in ables) able.EraseCache();
{
if (able is Equation equ) equ.EraseCache();
}
} }
} }
} }

View File

@ -1,18 +1,7 @@
using System; namespace Graphing.Forms;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Graphing.Forms public partial class SetZoomForm : Form
{ {
public partial class SetZoomForm : Form
{
private double minZoomRange; private double minZoomRange;
private double maxZoomRange; private double maxZoomRange;
@ -127,5 +116,4 @@ namespace Graphing.Forms
ZoomMaxValue.Text = maxZoomRange.ToString("0.00"); ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
} }
} }
}
} }

View File

@ -1,4 +1,5 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts;
namespace Graphing; namespace Graphing;
@ -26,5 +27,8 @@ public abstract class Graphable
Name = "Unnamed Graphable."; Name = "Unnamed Graphable.";
} }
public abstract IEnumerable<Line2d> GetItemsToRender(in GraphForm graph); public abstract IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph);
public abstract void EraseCache();
public abstract long GetCacheBytes();
} }

View File

@ -1,4 +1,5 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts;
namespace Graphing.Graphables; namespace Graphing.Graphables;
@ -19,73 +20,73 @@ public class Equation : Graphable
cache = []; cache = [];
} }
public override IEnumerable<Line2d> GetItemsToRender(in GraphForm graph) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{ {
const int step = 10; const int step = 10;
double epsilon = Math.Abs(graph.ScreenSpaceToGraphSpace(new Int2(0, 0)).x double epsilon = Math.Abs(graph.ScreenSpaceToGraphSpace(new Int2(0, 0)).x
- graph.ScreenSpaceToGraphSpace(new Int2(step / 2, 0)).x) / 5; - graph.ScreenSpaceToGraphSpace(new Int2(step / 2, 0)).x) / 5;
List<Line2d> lines = []; List<IGraphPart> lines = [];
bool addedToDictionary = false;
double previousX = graph.MinVisibleGraph.x; double previousX = graph.MinVisibleGraph.x;
double previousY = GetFromCache(previousX, epsilon, ref addedToDictionary); double previousY = GetFromCache(previousX, epsilon);
for (int i = 1; i < graph.ClientRectangle.Width; i += step) for (int i = 1; i < graph.ClientRectangle.Width; i += step)
{ {
double currentX = graph.ScreenSpaceToGraphSpace(new Int2(i, 0)).x; double currentX = graph.ScreenSpaceToGraphSpace(new Int2(i, 0)).x;
double currentY = GetFromCache(currentX, epsilon, ref addedToDictionary); double currentY = GetFromCache(currentX, epsilon);
if (Math.Abs(currentY - previousY) <= 10) if (Math.Abs(currentY - previousY) <= 10)
{ {
lines.Add(new Line2d(new Float2(previousX, previousY), new Float2(currentX, currentY))); lines.Add(new GraphLine(new Float2(previousX, previousY), new Float2(currentX, currentY)));
} }
previousX = currentX; previousX = currentX;
previousY = currentY; previousY = currentY;
} }
if (addedToDictionary) cache.Sort((a, b) => a.y.CompareTo(b.y)); // if (addedToDictionary) cache.Sort((a, b) => a.y.CompareTo(b.y)); todo: not required until binary search
return lines; return lines;
} }
public EquationDelegate GetDelegate() => equ; public EquationDelegate GetDelegate() => equ;
public void EraseCache() => cache.Clear(); public override void EraseCache() => cache.Clear();
private double GetFromCache(double x, double epsilon, ref bool addedToDictionary) private double GetFromCache(double x, double epsilon)
{ {
(double dist, double nearest) = NearestCachedPoint(x); (double dist, double nearest, int index) = NearestCachedPoint(x);
if (dist < epsilon) return nearest; if (dist < epsilon) return nearest;
else else
{ {
addedToDictionary = true;
double result = equ(x); double result = equ(x);
cache.Add(new(x, result)); cache.Add(new(x, result));
// TODO: Rather than sorting the whole list when we add a single number, // TODO: Rather than sorting the whole list when we add a single number,
// we could just insert it. // we could just insert it in its proper place.
return result; return result;
} }
} }
private (double dist, double y) NearestCachedPoint(double x) private (double dist, double y, int index) NearestCachedPoint(double x)
{ {
// TODO: Replace with a binary search system. // TODO: Replace with a binary search system.
double closestDist = double.PositiveInfinity; double closestDist = double.PositiveInfinity;
double closest = 0; double closest = 0;
int closestIndex = -1;
foreach (Float2 p in cache) for (int i = 0; i < cache.Count; i++)
{ {
double dist = Math.Abs(x - p.x); double dist = Math.Abs(x - cache[i].x);
if (dist < closestDist) if (dist < closestDist)
{ {
closestDist = dist; closestDist = dist;
closest = p.y; closest = cache[i].y;
closestIndex = i;
} }
} }
return (closestDist, closest); return (closestDist, closest, closestIndex);
} }
public long GetCacheBytes() => cache.Count * 16; public override long GetCacheBytes() => cache.Count * 16;
} }
public delegate double EquationDelegate(double x); public delegate double EquationDelegate(double x);

View File

@ -1,4 +1,6 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts;
using static System.Windows.Forms.LinkLabel;
namespace Graphing.Graphables; namespace Graphing.Graphables;
@ -9,6 +11,8 @@ public class SlopeField : Graphable
private readonly SlopeFieldsDelegate equ; private readonly SlopeFieldsDelegate equ;
private readonly double detail; private readonly double detail;
private readonly List<(Float2, GraphLine)> cache;
public SlopeField(int detail, SlopeFieldsDelegate equ) public SlopeField(int detail, SlopeFieldsDelegate equ)
{ {
slopeFieldNum++; slopeFieldNum++;
@ -16,25 +20,26 @@ public class SlopeField : Graphable
this.equ = equ; this.equ = equ;
this.detail = detail; this.detail = detail;
cache = [];
} }
public override IEnumerable<Line2d> GetItemsToRender(in GraphForm graph) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{ {
List<Line2d> lines = []; double epsilon = 1 / (detail * 2);
List<IGraphPart> lines = [];
for (double x = Math.Ceiling(graph.MinVisibleGraph.x - 1); x < graph.MaxVisibleGraph.x + 1; x += 1 / detail) for (double x = Math.Ceiling(graph.MinVisibleGraph.x - 1); x < graph.MaxVisibleGraph.x + 1; x += 1 / detail)
{ {
for (double y = Math.Ceiling(graph.MinVisibleGraph.y - 1); y < graph.MaxVisibleGraph.y + 1; y += 1 / detail) for (double y = Math.Ceiling(graph.MinVisibleGraph.y - 1); y < graph.MaxVisibleGraph.y + 1; y += 1 / detail)
{ {
double slope = equ(x, y); lines.Add(GetFromCache(epsilon, x, y));
lines.Add(MakeSlopeLine(new Float2(x, y), slope));
} }
} }
return lines; return lines;
} }
private Line2d MakeSlopeLine(Float2 position, double slope) private GraphLine MakeSlopeLine(Float2 position, double slope)
{ {
double size = detail; double size = detail;
@ -46,6 +51,28 @@ public class SlopeField : Graphable
return new(new(position.x + dirX, position.y + dirY), new(position.x - dirX, position.y - dirY)); return new(new(position.x + dirX, position.y + dirY), new(position.x - dirX, position.y - dirY));
} }
private GraphLine GetFromCache(double epsilon, double x, double y)
{
// Probably no binary search here, though maybe it could be done
// in terms of just one axis.
foreach ((Float2 p, GraphLine l) in cache)
{
double diffX = Math.Abs(p.x - x),
diffY = Math.Abs(p.y - y);
if (diffX < epsilon && diffY < epsilon) return l;
}
// Create a new value.
double slope = equ(x, y);
GraphLine result = MakeSlopeLine(new Float2(x, y), slope);
cache.Add((new Float2(x, y), result));
return result;
}
public override void EraseCache() => cache.Clear();
public override long GetCacheBytes() => cache.Count * 48;
} }
public delegate double SlopeFieldsDelegate(double x, double y); public delegate double SlopeFieldsDelegate(double x, double y);

View File

@ -0,0 +1,47 @@
using Graphing.Forms;
using Graphing.Parts;
namespace Graphing.Graphables;
public class TangentLine : Graphable
{
public double Position { get; set; }
private readonly EquationDelegate parentEqu;
private readonly double length;
public TangentLine(double length, double position, Equation parent)
{
Name = $"Tangent Line of {parent.Name}";
parentEqu = parent.GetDelegate();
Position = position;
this.length = length;
}
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
Float2 point = new(Position, parentEqu(Position));
return [MakeSlopeLine(point, DerivativeAtPoint(Position)),
new GraphCircle(point, 8)];
}
private GraphLine MakeSlopeLine(Float2 position, double slope)
{
double dirX = length, dirY = slope * length;
double magnitude = Math.Sqrt(dirX * dirX + dirY * dirY);
dirX /= magnitude * 2 / length;
dirY /= magnitude * 2 / length;
return new(new(position.x + dirX, position.y + dirY), new(position.x - dirX, position.y - dirY));
}
private double DerivativeAtPoint(double x)
{
const double step = 1e-3;
return (parentEqu(x + step) - parentEqu(x)) / step;
}
public override void EraseCache() { }
public override long GetCacheBytes() => 0;
}

8
Base/IGraphPart.cs Normal file
View File

@ -0,0 +1,8 @@
using Graphing.Forms;
namespace Graphing;
public interface IGraphPart
{
public void Render(in GraphForm form, in Graphics g, in Brush brush);
}

View File

@ -1,18 +0,0 @@
namespace Graphing;
public record struct Line2d
{
public Float2 a;
public Float2 b;
public Line2d()
{
a = new();
b = new();
}
public Line2d(Float2 a, Float2 b)
{
this.a = a;
this.b = b;
}
}

31
Base/Parts/GraphCircle.cs Normal file
View File

@ -0,0 +1,31 @@
using Graphing.Forms;
namespace Graphing.Parts;
public record struct GraphCircle : IGraphPart
{
public Float2 center;
public int radius;
public GraphCircle()
{
center = new();
radius = 1;
}
public GraphCircle(Float2 center, int radius)
{
this.center = center;
this.radius = radius;
}
public readonly void Render(in GraphForm form, in Graphics g, in Brush brush)
{
if (!double.IsNormal(center.x) || !double.IsNormal(center.y) ||
!double.IsNormal(radius)) return;
Int2 centerPix = form.GraphSpaceToScreenSpace(center);
g.FillEllipse(brush, new Rectangle(new Point(centerPix.x - radius,
centerPix.y - radius),
new Size(radius * 2, radius * 2)));
}
}

32
Base/Parts/GraphLine.cs Normal file
View File

@ -0,0 +1,32 @@
using Graphing.Forms;
namespace Graphing.Parts;
public record struct GraphLine : IGraphPart
{
public Float2 a;
public Float2 b;
public GraphLine()
{
a = new();
b = new();
}
public GraphLine(Float2 a, Float2 b)
{
this.a = a;
this.b = b;
}
public readonly void Render(in GraphForm form, in Graphics g, in Brush brush)
{
if (!double.IsNormal(a.x) || !double.IsNormal(a.y) ||
!double.IsNormal(b.x) || !double.IsNormal(b.y)) return;
Int2 start = form.GraphSpaceToScreenSpace(a),
end = form.GraphSpaceToScreenSpace(b);
Pen pen = new(brush, 3);
g.DrawLine(pen, start, end);
}
}

View File

@ -10,12 +10,21 @@ internal static class Program
{ {
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); Application.SetHighDpiMode(HighDpiMode.SystemAware);
GraphForm graph = new("One Of The Graphing Calculators Of All Time"); GraphForm graph = new("One Of The Graphing Calculators Of All Time");
graph.Graph(new Equation(x => Math.Pow(2, x)));
graph.Graph(new Equation(Math.Log2)); Equation equ = new(x => x * x);
TangentLine tangent = new(5, 2, equ);
graph.Graph(equ);
graph.Graph(tangent);
Application.Run(graph); Application.Run(graph);
} }
private static double PopulationGraph(double max, double k, double A, double t)
{
return max / (1 + A * Math.Exp(-k * t));
}
} }