Annotations.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. namespace Terminal.Gui;
  2. /// <summary>
  3. /// <para>Describes an overlay element that is rendered either before or after a series.</para>
  4. /// <para>
  5. /// Annotations can be positioned either in screen space (e.g. a legend) or in graph space (e.g. a line showing
  6. /// high point)
  7. /// </para>
  8. /// <para>Unlike <see cref="ISeries"/>, annotations are allowed to draw into graph margins</para>
  9. /// </summary>
  10. public interface IAnnotation
  11. {
  12. /// <summary>
  13. /// True if annotation should be drawn before <see cref="ISeries"/>. This allows Series and later annotations to
  14. /// potentially draw over the top of this annotation.
  15. /// </summary>
  16. bool BeforeSeries { get; }
  17. /// <summary>
  18. /// Called once after series have been rendered (or before if <see cref="BeforeSeries"/> is true). Use
  19. /// <see cref="View.Driver"/> to draw and <see cref="View.Viewport"/> to avoid drawing outside of graph
  20. /// </summary>
  21. /// <param name="graph"></param>
  22. void Render (GraphView graph);
  23. }
  24. /// <summary>Displays text at a given position (in screen space or graph space)</summary>
  25. public class TextAnnotation : IAnnotation
  26. {
  27. /// <summary>
  28. /// The location in graph space to draw the <see cref="Text"/>. This annotation will only show if the point is in
  29. /// the current viewable area of the graph presented in the <see cref="GraphView"/>
  30. /// </summary>
  31. public PointF GraphPosition { get; set; }
  32. /// <summary>
  33. /// The location on screen to draw the <see cref="Text"/> regardless of scroll/zoom settings. This overrides
  34. /// <see cref="GraphPosition"/> if specified.
  35. /// </summary>
  36. public Point? ScreenPosition { get; set; }
  37. /// <summary>Text to display on the graph</summary>
  38. public string Text { get; set; }
  39. /// <summary>True to add text before plotting series. Defaults to false</summary>
  40. public bool BeforeSeries { get; set; }
  41. /// <summary>Draws the annotation</summary>
  42. /// <param name="graph"></param>
  43. public void Render (GraphView graph)
  44. {
  45. if (ScreenPosition.HasValue)
  46. {
  47. DrawText (graph, ScreenPosition.Value.X, ScreenPosition.Value.Y);
  48. return;
  49. }
  50. Point screenPos = graph.GraphSpaceToScreen (GraphPosition);
  51. DrawText (graph, screenPos.X, screenPos.Y);
  52. }
  53. /// <summary>
  54. /// Draws the <see cref="Text"/> at the given coordinates with truncation to avoid spilling over
  55. /// <see name="View.Viewport"/> of the <paramref name="graph"/>
  56. /// </summary>
  57. /// <param name="graph"></param>
  58. /// <param name="x">Screen x position to start drawing string</param>
  59. /// <param name="y">Screen y position to start drawing string</param>
  60. protected void DrawText (GraphView graph, int x, int y)
  61. {
  62. // the draw point is out of control bounds
  63. if (!graph.Viewport.Contains (new Point (x, y)))
  64. {
  65. return;
  66. }
  67. // There is no text to draw
  68. if (string.IsNullOrWhiteSpace (Text))
  69. {
  70. return;
  71. }
  72. graph.Move (x, y);
  73. int availableWidth = graph.Viewport.Width - x;
  74. if (availableWidth <= 0)
  75. {
  76. return;
  77. }
  78. if (Text.Length < availableWidth)
  79. {
  80. View.Driver.AddStr (Text);
  81. }
  82. else
  83. {
  84. View.Driver.AddStr (Text.Substring (0, availableWidth));
  85. }
  86. }
  87. }
  88. /// <summary>A box containing symbol definitions e.g. meanings for colors in a graph. The 'Key' to the graph</summary>
  89. public class LegendAnnotation : View, IAnnotation
  90. {
  91. /// <summary>Ordered collection of entries that are rendered in the legend.</summary>
  92. private readonly List<Tuple<GraphCellToRender, string>> _entries = new ();
  93. /// <summary>Creates a new empty legend at the empty screen coordinates.</summary>
  94. public LegendAnnotation () : this (Rectangle.Empty) { }
  95. /// <summary>Creates a new empty legend at the given screen coordinates.</summary>
  96. /// <param name="legendBounds">
  97. /// Defines the area available for the legend to render in (within the graph). This is in
  98. /// screen units (i.e. not graph space)
  99. /// </param>
  100. public LegendAnnotation (Rectangle legendBounds)
  101. {
  102. X = legendBounds.X;
  103. Y = legendBounds.Y;
  104. Width = legendBounds.Width;
  105. Height = legendBounds.Height;
  106. BorderStyle = LineStyle.Single;
  107. }
  108. /// <summary>Returns false i.e. Legends render after series</summary>
  109. public bool BeforeSeries => false;
  110. /// <summary>Draws the Legend and all entries into the area within <see cref="View.Viewport"/></summary>
  111. /// <param name="graph"></param>
  112. public void Render (GraphView graph)
  113. {
  114. if (!IsInitialized)
  115. {
  116. ColorScheme = new ColorScheme { Normal = Application.Driver?.GetAttribute () ?? Attribute.Default};
  117. graph.Add (this);
  118. }
  119. if (BorderStyle != LineStyle.None)
  120. {
  121. OnDrawAdornments ();
  122. OnRenderLineCanvas ();
  123. }
  124. var linesDrawn = 0;
  125. foreach (Tuple<GraphCellToRender, string> entry in _entries)
  126. {
  127. if (entry.Item1.Color.HasValue)
  128. {
  129. Application.Driver?.SetAttribute (entry.Item1.Color.Value);
  130. }
  131. else
  132. {
  133. graph.SetDriverColorToGraphColor ();
  134. }
  135. // add the symbol
  136. AddRune (0, linesDrawn, entry.Item1.Rune);
  137. // switch to normal coloring (for the text)
  138. graph.SetDriverColorToGraphColor ();
  139. // add the text
  140. Move (1, linesDrawn);
  141. string str = TextFormatter.ClipOrPad (entry.Item2, Viewport.Width - 1);
  142. Application.Driver?.AddStr (str);
  143. linesDrawn++;
  144. // Legend has run out of space
  145. if (linesDrawn >= Viewport.Height)
  146. {
  147. break;
  148. }
  149. }
  150. }
  151. /// <summary>Adds an entry into the legend. Duplicate entries are permissible</summary>
  152. /// <param name="graphCellToRender">The symbol appearing on the graph that should appear in the legend</param>
  153. /// <param name="text">
  154. /// Text to render on this line of the legend. Will be truncated if outside of Legend
  155. /// <see cref="View.Viewport"/>
  156. /// </param>
  157. public void AddEntry (GraphCellToRender graphCellToRender, string text) { _entries.Add (Tuple.Create (graphCellToRender, text)); }
  158. }
  159. /// <summary>Sequence of lines to connect points e.g. of a <see cref="ScatterSeries"/></summary>
  160. public class PathAnnotation : IAnnotation
  161. {
  162. /// <summary>Color for the line that connects points</summary>
  163. public Attribute? LineColor { get; set; }
  164. /// <summary>The symbol that gets drawn along the line, defaults to '.'</summary>
  165. public Rune LineRune { get; set; } = new ('.');
  166. /// <summary>Points that should be connected. Lines will be drawn between points in the order they appear in the list</summary>
  167. public List<PointF> Points { get; set; } = new ();
  168. /// <summary>True to add line before plotting series. Defaults to false</summary>
  169. public bool BeforeSeries { get; set; }
  170. /// <summary>Draws lines connecting each of the <see cref="Points"/></summary>
  171. /// <param name="graph"></param>
  172. public void Render (GraphView graph)
  173. {
  174. View.Driver.SetAttribute (LineColor ?? graph.ColorScheme.Normal);
  175. foreach (LineF line in PointsToLines ())
  176. {
  177. Point start = graph.GraphSpaceToScreen (line.Start);
  178. Point end = graph.GraphSpaceToScreen (line.End);
  179. graph.DrawLine (start, end, LineRune);
  180. }
  181. }
  182. /// <summary>Generates lines joining <see cref="Points"/></summary>
  183. /// <returns></returns>
  184. private IEnumerable<LineF> PointsToLines ()
  185. {
  186. for (var i = 0; i < Points.Count - 1; i++)
  187. {
  188. yield return new LineF (Points [i], Points [i + 1]);
  189. }
  190. }
  191. }