Annotations.cs 8.2 KB

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