using System; using System.Collections.Generic; using System.Linq; namespace Terminal.Gui { /// /// Describes an overlay element that is rendered either before or /// after a series. /// /// Annotations can be positioned either in screen space (e.g. /// a legend) or in graph space (e.g. a line showing high point) /// /// Unlike , annotations are allowed to /// draw into graph margins /// /// public interface IAnnotation { /// /// True if annotation should be drawn before . This /// allowes Series and later annotations to potentially draw over the top /// of this annotation. /// bool BeforeSeries { get; } /// /// Called once after series have been rendered (or before if is true). /// Use to draw and to avoid drawing outside of /// graph /// /// void Render (GraphView graph); } /// /// Displays text at a given position (in screen space or graph space) /// public class TextAnnotation : IAnnotation { /// /// The location on screen to draw the regardless /// of scroll/zoom settings. This overrides /// if specified. /// public Point? ScreenPosition { get; set; } /// /// The location in graph space to draw the . This /// annotation will only show if the point is in the current viewable /// area of the graph presented in the /// public PointF GraphPosition { get; set; } /// /// Text to display on the graph /// public string Text { get; set; } /// /// True to add text before plotting series. Defaults to false /// public bool BeforeSeries { get; set; } /// /// Draws the annotation /// /// public void Render (GraphView graph) { if (ScreenPosition.HasValue) { DrawText (graph, ScreenPosition.Value.X, ScreenPosition.Value.Y); return; } var screenPos = graph.GraphSpaceToScreen (GraphPosition); DrawText (graph, screenPos.X, screenPos.Y); } /// /// Draws the at the given coordinates with truncation to avoid /// spilling over of the /// /// /// Screen x position to start drawing string /// Screen y position to start drawing string protected void DrawText (GraphView graph, int x, int y) { // the draw point is out of control bounds if (!graph.Bounds.Contains (new Point (x, y))) { return; } // There is no text to draw if (string.IsNullOrWhiteSpace (Text)) { return; } graph.Move (x, y); int availableWidth = graph.Bounds.Width - x; if (availableWidth <= 0) { return; } if (Text.Length < availableWidth) { View.Driver.AddStr (Text); } else { View.Driver.AddStr (Text.Substring (0, availableWidth)); } } } /// /// A box containing symbol definitions e.g. meanings for colors in a graph. /// The 'Key' to the graph /// public class LegendAnnotation : IAnnotation { /// /// True to draw a solid border around the legend. /// Defaults to true. This border will be within the /// and so reduces the width/height /// available for text by 2 /// public bool Border { get; set; } = true; /// /// Defines the screen area available for the legend to render in /// public Rect Bounds { get; set; } /// /// Returns false i.e. Lengends render after series /// public bool BeforeSeries => false; /// /// Ordered collection of entries that are rendered in the legend. /// List> entries = new List> (); /// /// Creates a new empty legend at the given screen coordinates /// /// Defines the area available for the legend to render in /// (within the graph). This is in screen units (i.e. not graph space) public LegendAnnotation (Rect legendBounds) { Bounds = legendBounds; } /// /// Draws the Legend and all entries into the area within /// /// public void Render (GraphView graph) { if (Border) { graph.Border.DrawFrame (Bounds, true); } // start the legend at int y = Bounds.Top + (Border ? 1 : 0); int x = Bounds.Left + (Border ? 1 : 0); // how much horizontal space is available for writing legend entries? int availableWidth = Bounds.Width - (Border ? 2 : 0); int availableHeight = Bounds.Height - (Border ? 2 : 0); int linesDrawn = 0; foreach (var entry in entries) { if (entry.Item1.Color.HasValue) { Application.Driver.SetAttribute (entry.Item1.Color.Value); } else { graph.SetDriverColorToGraphColor (); } // add the symbol graph.AddRune (x, y + linesDrawn, entry.Item1.Rune); // switch to normal coloring (for the text) graph.SetDriverColorToGraphColor (); // add the text graph.Move (x + 1, y + linesDrawn); string str = TextFormatter.ClipOrPad (entry.Item2, availableWidth - 1); Application.Driver.AddStr (str); linesDrawn++; // Legend has run out of space if (linesDrawn >= availableHeight) { break; } } } /// /// Adds an entry into the legend. Duplicate entries are permissable /// /// The symbol appearing on the graph that should appear in the legend /// Text to render on this line of the legend. Will be truncated /// if outside of Legend public void AddEntry (GraphCellToRender graphCellToRender, string text) { entries.Add (Tuple.Create (graphCellToRender, text)); } } /// /// Sequence of lines to connect points e.g. of a /// public class PathAnnotation : IAnnotation { /// /// Points that should be connected. Lines will be drawn between points in the order /// they appear in the list /// public List Points { get; set; } = new List (); /// /// Color for the line that connects points /// public Attribute? LineColor { get; set; } /// /// The symbol that gets drawn along the line, defaults to '.' /// public Rune LineRune { get; set; } = new Rune ('.'); /// /// True to add line before plotting series. Defaults to false /// public bool BeforeSeries { get; set; } /// /// Draws lines connecting each of the /// /// public void Render (GraphView graph) { View.Driver.SetAttribute (LineColor ?? graph.ColorScheme.Normal); foreach (var line in PointsToLines ()) { var start = graph.GraphSpaceToScreen (line.Start); var end = graph.GraphSpaceToScreen (line.End); graph.DrawLine (start, end, LineRune); } } /// /// Generates lines joining /// /// private IEnumerable PointsToLines () { for (int i = 0; i < Points.Count - 1; i++) { yield return new LineF (Points [i], Points [i + 1]); } } /// /// Describes two points in graph space and a line between them /// public class LineF { /// /// The start of the line /// public PointF Start { get; } /// /// The end point of the line /// public PointF End { get; } /// /// Creates a new line between the points /// public LineF (PointF start, PointF end) { this.Start = start; this.End = end; } } } }