From 265c86ede5cf99b43976faa4ca536b3c8c7dd58a Mon Sep 17 00:00:00 2001 From: Dillon Date: Sat, 14 Dec 2024 17:55:36 -0800 Subject: [PATCH] Duplicate ComputerAlgebra.Plotting and remove dependency --- ComputerAlgebra | 2 +- LiveSPICE.sln | 6 - Tests/Plotting/Plot.cs | 270 +++++++++++++++++++++++++++++ Tests/Plotting/Series.cs | 109 ++++++++++++ Tests/Plotting/SeriesCollection.cs | 168 ++++++++++++++++++ Tests/Test.cs | 2 +- Tests/Tests.csproj | 2 +- 7 files changed, 550 insertions(+), 9 deletions(-) create mode 100644 Tests/Plotting/Plot.cs create mode 100644 Tests/Plotting/Series.cs create mode 100644 Tests/Plotting/SeriesCollection.cs diff --git a/ComputerAlgebra b/ComputerAlgebra index 64c51e89..20634f1c 160000 --- a/ComputerAlgebra +++ b/ComputerAlgebra @@ -1 +1 @@ -Subproject commit 64c51e893d6c2dc497eb4dc4f87d278529d93009 +Subproject commit 20634f1c2465471133abb166a26b321e0003fe5b diff --git a/LiveSPICE.sln b/LiveSPICE.sln index 0d7267d0..6bf97481 100644 --- a/LiveSPICE.sln +++ b/LiveSPICE.sln @@ -27,8 +27,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MockVst", "MockVst\MockVst. EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LiveSPICEVst", "LiveSPICEVst", "{6DD7DA6B-5277-45C3-ACBD-8306D27528D0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComputerAlgebra.Plotting", "ComputerAlgebra\ComputerAlgebra.Plotting\ComputerAlgebra.Plotting.csproj", "{51F8BCEC-5C29-46BA-8448-06B516715840}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{ECCE5E4E-730F-4A2D-A772-3AC922D9ACD2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{04388F7E-6B22-489C-A791-6B10372D8862}" @@ -88,10 +86,6 @@ Global {AD8DCC49-44C5-4E36-8F06-F100C5A7BAD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD8DCC49-44C5-4E36-8F06-F100C5A7BAD4}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD8DCC49-44C5-4E36-8F06-F100C5A7BAD4}.Release|Any CPU.Build.0 = Release|Any CPU - {51F8BCEC-5C29-46BA-8448-06B516715840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {51F8BCEC-5C29-46BA-8448-06B516715840}.Debug|Any CPU.Build.0 = Debug|Any CPU - {51F8BCEC-5C29-46BA-8448-06B516715840}.Release|Any CPU.ActiveCfg = Release|Any CPU - {51F8BCEC-5C29-46BA-8448-06B516715840}.Release|Any CPU.Build.0 = Release|Any CPU {ECCE5E4E-730F-4A2D-A772-3AC922D9ACD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ECCE5E4E-730F-4A2D-A772-3AC922D9ACD2}.Debug|Any CPU.Build.0 = Debug|Any CPU {ECCE5E4E-730F-4A2D-A772-3AC922D9ACD2}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Tests/Plotting/Plot.cs b/Tests/Plotting/Plot.cs new file mode 100644 index 00000000..24b633b8 --- /dev/null +++ b/Tests/Plotting/Plot.cs @@ -0,0 +1,270 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Threading; +using System.Windows.Forms; +using Matrix2D = System.Drawing.Drawing2D.Matrix; + +namespace Plotting +{ + /// + /// A single plot window. + /// + public class Plot + { + protected SeriesCollection series; + /// + /// The series displayed in this plot. + /// + public SeriesCollection Series { get { return series; } } + + /// + /// Width of the plot window. + /// + public int Width { get { return form.Width; } set { Invoke(() => form.Width = value); } } + /// + /// Height of the plot window. + /// + public int Height { get { return form.Height; } set { Invoke(() => form.Height = value); } } + + private double _x0 = -10.0, _x1 = 10.0; + private double _y0 = double.NaN, _y1 = double.NaN; + /// + /// Plot area bounds. + /// + public double x0 { get { return _x0; } set { Invoke(() => { _x0 = value; Invalidate(); }); } } + public double y0 { get { return _y0; } set { Invoke(() => { _y0 = value; Invalidate(); }); } } + public double x1 { get { return _x1; } set { Invoke(() => { _x1 = value; Invalidate(); }); } } + public double y1 { get { return _y1; } set { Invoke(() => { _y1 = value; Invalidate(); }); } } + + private string xlabel = null, ylabel = null; + /// + /// Axis labels. + /// + public string xLabel { get { return xlabel; } set { Invoke(() => { xlabel = value; Invalidate(); }); } } + public string yLabel { get { return ylabel; } set { Invoke(() => { ylabel = value; Invalidate(); }); } } + + string title = null; + /// + /// Title of the plot window. + /// + public string Title { get { return title; } set { Invoke(() => { form.Text = title = value; Invalidate(); }); } } + + private bool showLegend = true; + /// + /// Show or hide the legend. + /// + public bool ShowLegend { get { return showLegend; } set { Invoke(() => { showLegend = value; Invalidate(); }); } } + + protected Thread thread; + protected Form form = new Form(); + protected bool shown = false; + private void Invoke(Action action) + { + while (!shown) + Thread.Sleep(0); + form.Invoke((Delegate)action); + } + + public Plot() + { + series = new SeriesCollection(); + series.ItemAdded += (o, e) => Invalidate(); + series.ItemRemoved += (o, e) => Invalidate(); + + form = new Form() + { + Text = "Plot", + Width = 300, + Height = 300, + }; + form.Paint += Plot_Paint; + form.SizeChanged += Plot_SizeChanged; + form.Shown += (o, e) => shown = true; + form.KeyDown += (o, e) => { if (e.KeyCode == Keys.Escape) form.Close(); }; + + thread = new Thread(() => Application.Run(form)); + thread.Start(); + } + + public void Save(string Filename) + { + Rectangle bounds = new Rectangle(0, 0, Width, Height); + Bitmap bitmap = new Bitmap(Width, Height); + Invoke(() => form.DrawToBitmap(bitmap, bounds)); + bitmap.Save(Filename); + } + + private RectangleF PaintTitle(Graphics G, RectangleF Area) + { + if (title == null) + return Area; + + Font font = new Font(form.Font.FontFamily, form.Font.Size * 1.5f, FontStyle.Bold); + + // Draw title. + SizeF sz = G.MeasureString(title, font); + G.DrawString(title, font, Brushes.Black, new PointF(Area.Left + (Area.Width - sz.Width) / 2, Area.Top)); + Area.Y += sz.Height + 10.0f; + Area.Height -= sz.Height + 10.0f; + + return Area; + } + + private RectangleF PaintAxisLabels(Graphics G, RectangleF Area) + { + Font font = new Font(form.Font.FontFamily, form.Font.Size * 1.25f); + + // Draw axis labels. + if (xlabel != null) + { + SizeF sz = G.MeasureString(xlabel, font); + G.DrawString(xlabel, font, Brushes.Black, new PointF(Area.Left + (Area.Width - sz.Width) / 2, Area.Bottom - sz.Height - 5.0f)); + Area.Height -= sz.Height + 10.0f; + } + if (ylabel != null) + { + SizeF sz = G.MeasureString(ylabel, font); + G.TranslateTransform(Area.Left + 5.0f, Area.Top + (Area.Height + sz.Width) / 2.0f); + G.RotateTransform(-90.0f); + G.DrawString(ylabel, font, Brushes.Black, new PointF(0.0f, 0.0f)); + G.ResetTransform(); + + Area.X += sz.Height + 10.0f; + Area.Width -= sz.Height + 10.0f; + } + + Area.X += 30.0f; + Area.Width -= 30.0f; + Area.Height -= 20.0f; + + return Area; + } + + private RectangleF PaintLegend(Graphics G, RectangleF Area) + { + if (!showLegend) + return Area; + + Font font = form.Font; + + // Draw legend. + float legendWidth = 0.0f; + series.ForEach(i => + { + SizeF sz = G.MeasureString(i.Name, font); + legendWidth = Math.Max(legendWidth, sz.Width); + }); + + float legendY = Area.Top; + series.ForEach(i => + { + PointF lx = new PointF(Area.Right - legendWidth, legendY); + SizeF sz = G.MeasureString(i.Name, font); + G.DrawString(i.Name, font, Brushes.Black, lx); + + PointF[] points = new PointF[] + { + new PointF(lx.X - 25.0f, legendY + sz.Height / 2.0f), + new PointF(lx.X - 5.0f, legendY + sz.Height / 2.0f), + }; + G.DrawLines(i.Pen, points); + legendY += sz.Height; + }); + + Area.Width -= legendWidth + 30.0f; + + return Area; + } + + private void Plot_Paint(object sender, PaintEventArgs e) + { + if (series.Count == 0) + return; + + Graphics G = e.Graphics; + G.SmoothingMode = SmoothingMode.AntiAlias; + G.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; + + Pen axis = Pens.Black; + Pen grid = Pens.LightGray; + Font font = form.Font; + + // Compute plot area. + RectangleF area = e.ClipRectangle; + area.Inflate(-10.0f, -10.0f); + + area = PaintTitle(G, area); + area = PaintAxisLabels(G, area); + area = PaintLegend(G, area); + + // Draw background. + G.FillRectangle(Brushes.White, area); + G.DrawRectangle(Pens.Gray, area.Left, area.Top, area.Width, area.Height); + + // Compute plot bounds. + PointF x0, x1; + double y0 = _y0, y1 = _y1; + if (double.IsNaN(y0) || double.IsNaN(y1)) + series.AutoBounds(_x0, _x1, out y0, out y1); + x0 = new PointF((float)_x0, (float)y0); + x1 = new PointF((float)_x1, (float)y1); + + Matrix2D T = new Matrix2D( + area, + new PointF[] { new PointF(x0.X, x1.Y), new PointF(x1.X, x1.Y), new PointF(x0.X, x0.Y) }); + T.Invert(); + + // Draw axes. + double dx = Partition((x1.X - x0.X) / (Width / 80)); + for (double x = x0.X - x0.X % dx; x <= x1.X; x += dx) + { + PointF tx = Tx(T, new PointF((float)x, 0.0f)); + string s = x.ToString("G3"); + SizeF sz = G.MeasureString(s, font); + G.DrawString(s, font, Brushes.Black, new PointF(tx.X - sz.Width / 2, area.Bottom + 3.0f)); + G.DrawLine(grid, new PointF(tx.X, area.Top), new PointF(tx.X, area.Bottom)); + } + + double dy = Partition((x1.Y - x0.Y) / (Height / 50)); + for (double y = x0.Y - x0.Y % dy; y <= x1.Y; y += dy) + { + PointF tx = Tx(T, new PointF(0.0f, (float)y)); + string s = y.ToString("G3"); + SizeF sz = G.MeasureString(s, font); + G.DrawString(s, font, Brushes.Black, new PointF(area.Left - sz.Width, tx.Y - sz.Height / 2)); + G.DrawLine(grid, new PointF(area.Left, tx.Y), new PointF(area.Right, tx.Y)); + } + + G.DrawLine(axis, Tx(T, new PointF(x0.X, 0.0f)), Tx(T, new PointF(x1.X, 0.0f))); + G.DrawLine(axis, Tx(T, new PointF(0.0f, x0.Y)), Tx(T, new PointF(0.0f, x1.Y))); + G.DrawRectangle(Pens.Gray, area.Left, area.Top, area.Width, area.Height); + G.SetClip(area); + + // Draw series. + series.ForEach(i => i.Paint(T, x0.X, x1.X, G)); + } + + private static PointF Tx(Matrix2D T, PointF x) + { + PointF[] xs = new[] { x }; + T.TransformPoints(xs); + return xs[0]; + } + + private void Invalidate() { form.Invalidate(); } + + private void Plot_SizeChanged(object sender, EventArgs e) { form.Invalidate(); } + + private static double Partition(double P) + { + double[] Partitions = { 10.0, 4.0, 2.0 }; + + double p = Math.Pow(10.0, Math.Ceiling(Math.Log10(P))); + foreach (double i in Partitions) + if (p / i > P) + return p / i; + return p; + } + } +} diff --git a/Tests/Plotting/Series.cs b/Tests/Plotting/Series.cs new file mode 100644 index 00000000..563c37c5 --- /dev/null +++ b/Tests/Plotting/Series.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using Matrix2D = System.Drawing.Drawing2D.Matrix; + +namespace Plotting +{ + public enum PointStyle + { + None, + Square, + Circle, + Cross, + } + + /// + /// A data series. + /// + public abstract class Series + { + private string name = "y[x]"; + public string Name { get { return name; } set { name = value; } } + + private Pen pen = null; + public Pen Pen { get { return pen != null ? pen : Pens.Transparent; } set { pen = value; } } + + protected PointStyle pointStyle = PointStyle.Square; + public PointStyle PointStyle { get { return pointStyle; } set { pointStyle = value; } } + + public abstract List Evaluate(double x0, double x1); + + public void Paint(Matrix2D T, double x0, double x1, Graphics G) + { + Pen pen = Pen; + List points = Evaluate(x0, x1); + foreach (PointF[] i in points) + { + T.TransformPoints(i); + G.DrawLines(pen, i); + } + } + + protected static float ToFloat(double x) + { + if (x > 1e6) + return 1e6f; + else if (x < -1e6) + return -1e6f; + else + return (float)x; + } + } + + /// + /// Data series derived from a lambda function. + /// + public class Function : Series + { + protected Func f; + public Function(Func f) { this.f = f; } + + public override List Evaluate(double x0, double x1) + { + int N = 2048; + + List points = new List(); + + List run = new List(); + for (int i = 0; i <= N; ++i) + { + double x = ((x1 - x0) * i) / N + x0; + float fx = ToFloat(f(x)); + + if (double.IsNaN(fx) || float.IsInfinity(fx)) + { + if (run.Count > 1) + points.Add(run.ToArray()); + run.Clear(); + } + else + { + run.Add(new PointF((float)x, fx)); + } + } + if (run.Count > 1) + points.Add(run.ToArray()); + + return points; + } + } + + /// + /// Explicit point list. + /// + public class Scatter : Series + { + protected PointF[] points; + public Scatter(KeyValuePair[] Points) + { + points = Points.Select(i => new PointF((float)i.Key, ToFloat(i.Value))).ToArray(); + } + + public override List Evaluate(double x0, double x1) + { + return new List() { points.ToArray() }; + } + } +} diff --git a/Tests/Plotting/SeriesCollection.cs b/Tests/Plotting/SeriesCollection.cs new file mode 100644 index 00000000..625875da --- /dev/null +++ b/Tests/Plotting/SeriesCollection.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace Plotting +{ + public class SeriesEventArgs : EventArgs + { + private Series s; + public Series Series { get { return s; } } + + public SeriesEventArgs(Series S) { s = S; } + } + + /// + /// Collection of Series. + /// + public class SeriesCollection : ICollection, IEnumerable + { + protected List x = new List(); + protected List colors = new List() + { + Color.Red, + Color.Blue, + Color.Green, + Color.DarkRed, + Color.DarkGreen, + Color.DarkBlue, + }; + + /// + /// Default colors used for series if none is specified. + /// + public IList Colors { get { return colors; } } + + /// + /// Get the series at the specified index. + /// + /// + /// + public Series this[int i] { get { return x[i]; } } + + /// + /// Estimate useful bounds for displaying the signals in this collection. + /// + /// + /// + /// + /// + public void AutoBounds(double x0, double x1, out double y0, out double y1) + { + lock (x) + { + int N = 0; + + // Compute the mean. + float mean = 0.0f; + x.ForEach(i => + { + List xy = i.Evaluate(x0, x1); + mean += xy.Sum(j => j.Sum(k => k.Y)); + N += xy.Sum(j => j.Count()); + }); + mean /= N; + + // Compute standard deviation. + float stddev = 0.0f; + float max = 0.0f; + //series.ForEach(i => stddev = Math.Max(stddev, i.Evaluate(_x0, _x1).Max(j => j.Max(k => Math.Abs(mean - k.Y))) * 1.25 + 1e-6)); + x.ForEach(i => + { + List xy = i.Evaluate(x0, x1); + stddev += xy.Sum(j => j.Sum(k => (mean - k.Y) * (mean - k.Y))); + max = xy.Max(j => j.Max(k => Math.Abs(mean - k.Y)), max); + }); + stddev = (float)Math.Sqrt(stddev / N) * 4.0f; + float y = Math.Min(stddev, max) * 1.25f + 1e-6f; + + y0 = mean - y; + y1 = mean + y; + } + } + + public delegate void SeriesEventHandler(object sender, SeriesEventArgs e); + + private List itemAdded = new List(); + protected void OnItemAdded(SeriesEventArgs e) { foreach (SeriesEventHandler i in itemAdded) i(this, e); } + /// + /// Called when a series is added to the collection. + /// + public event SeriesEventHandler ItemAdded + { + add { itemAdded.Add(value); } + remove { itemAdded.Remove(value); } + } + + private List itemRemoved = new List(); + protected void OnItemRemoved(SeriesEventArgs e) { foreach (SeriesEventHandler i in itemRemoved) i(this, e); } + /// + /// Called when a series is removed from the collection. + /// + public event SeriesEventHandler ItemRemoved + { + add { itemRemoved.Add(value); } + remove { itemRemoved.Remove(value); } + } + + // ICollection + public int Count { get { lock (x) return x.Count; } } + public bool IsReadOnly { get { return false; } } + public void Add(Series item) + { + lock (x) + { + + if (item.Pen == Pens.Transparent) + item.Pen = new Pen(colors.ArgMin(j => x.Count(k => k.Pen != null && k.Pen.Color == j)), 0.5f); + x.Add(item); + } + OnItemAdded(new SeriesEventArgs(item)); + } + public void AddRange(IEnumerable items) + { + lock (x) foreach (Series i in items) + Add(i); + } + public void Clear() + { + Series[] removed; + lock (x) + { + removed = x.ToArray(); + x.Clear(); + } + + foreach (Series i in removed) + OnItemRemoved(new SeriesEventArgs(i)); + } + public bool Contains(Series item) { lock (x) return x.Contains(item); } + public void CopyTo(Series[] array, int arrayIndex) { lock (x) x.CopyTo(array, arrayIndex); } + public bool Remove(Series item) + { + bool ret; + lock (x) ret = x.Remove(item); + if (ret) + OnItemRemoved(new SeriesEventArgs(item)); + return ret; + } + public void RemoveRange(IEnumerable items) + { + foreach (Series i in items) + Remove(i); + } + + public void ForEach(Action f) + { + lock (x) foreach (Series i in x) + f(i); + } + + // IEnumerable + public IEnumerator GetEnumerator() { return x.GetEnumerator(); } + + IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } + } +} diff --git a/Tests/Test.cs b/Tests/Test.cs index 254a159d..c2244990 100644 --- a/Tests/Test.cs +++ b/Tests/Test.cs @@ -1,6 +1,6 @@ using Circuit; using ComputerAlgebra; -using ComputerAlgebra.Plotting; +using Plotting; using System; using System.Collections.Generic; using System.IO; diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 12e7da5b..3c091cb1 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -9,10 +9,10 @@ 1.0.0.0 8 enable + true -