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 allows 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 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; } /// /// The location on screen to draw the regardless of scroll/zoom settings. This overrides /// if specified. /// public Point? ScreenPosition { 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; } Point 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.Viewport.Contains (new Point (x, y))) { return; } // There is no text to draw if (string.IsNullOrWhiteSpace (Text)) { return; } graph.Move (x, y); int availableWidth = graph.Viewport.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 : View, IAnnotation { /// Ordered collection of entries that are rendered in the legend. private readonly List> _entries = new (); /// Creates a new empty legend at the empty screen coordinates. public LegendAnnotation () : this (Rectangle.Empty) { } /// 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 (Rectangle legendBounds) { X = legendBounds.X; Y = legendBounds.Y; Width = legendBounds.Width; Height = legendBounds.Height; BorderStyle = LineStyle.Single; } /// Returns false i.e. Legends render after series public bool BeforeSeries => false; // BUGBUG: Legend annotations are subviews. But for some reason the are rendered directly in OnDrawContent // BUGBUG: instead of just being normal subviews. They get rendered as blank rects and thus we disable subview drawing. /// protected override bool OnDrawingText () { return true; } // BUGBUG: Legend annotations are subviews. But for some reason the are rendered directly in OnDrawContent // BUGBUG: instead of just being normal subviews. They get rendered as blank rects and thus we disable subview drawing. /// protected override bool OnClearingViewport () { return true; } /// Draws the Legend and all entries into the area within /// public void Render (GraphView graph) { if (!IsInitialized) { ColorScheme = new ColorScheme { Normal = Application.Driver?.GetAttribute () ?? Attribute.Default}; graph.Add (this); } if (BorderStyle != LineStyle.None) { DrawAdornments (); RenderLineCanvas (); } var linesDrawn = 0; foreach (Tuple entry in _entries) { if (entry.Item1.Color.HasValue) { SetAttribute (entry.Item1.Color.Value); } else { graph.SetDriverColorToGraphColor (); } // add the symbol AddRune (0, linesDrawn, entry.Item1.Rune); // switch to normal coloring (for the text) graph.SetDriverColorToGraphColor (); // add the text Move (1, linesDrawn); string str = TextFormatter.ClipOrPad (entry.Item2, Viewport.Width - 1); Application.Driver?.AddStr (str); linesDrawn++; // Legend has run out of space if (linesDrawn >= Viewport.Height) { break; } } } /// Adds an entry into the legend. Duplicate entries are permissible /// 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 { /// 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 ('.'); /// 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 (); /// 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) { graph.SetAttribute (LineColor ?? graph.ColorScheme.Normal); foreach (LineF line in PointsToLines ()) { Point start = graph.GraphSpaceToScreen (line.Start); Point end = graph.GraphSpaceToScreen (line.End); graph.DrawLine (start, end, LineRune); } } /// Generates lines joining /// private IEnumerable PointsToLines () { for (var i = 0; i < Points.Count - 1; i++) { yield return new LineF (Points [i], Points [i + 1]); } } }