GraphView.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. using System.Text;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. namespace Terminal.Gui {
  6. /// <summary>
  7. /// Control for rendering graphs (bar, scatter etc)
  8. /// </summary>
  9. public class GraphView : View {
  10. /// <summary>
  11. /// Horizontal axis
  12. /// </summary>
  13. /// <value></value>
  14. public HorizontalAxis AxisX { get; set; }
  15. /// <summary>
  16. /// Vertical axis
  17. /// </summary>
  18. /// <value></value>
  19. public VerticalAxis AxisY { get; set; }
  20. /// <summary>
  21. /// Collection of data series that are rendered in the graph
  22. /// </summary>
  23. public List<ISeries> Series { get; } = new List<ISeries> ();
  24. /// <summary>
  25. /// Elements drawn into graph after series have been drawn e.g. Legends etc
  26. /// </summary>
  27. public List<IAnnotation> Annotations { get; } = new List<IAnnotation> ();
  28. /// <summary>
  29. /// Amount of space to leave on left of control. Graph content (<see cref="Series"/>)
  30. /// will not be rendered in margins but axis labels may be
  31. /// </summary>
  32. public uint MarginLeft { get; set; }
  33. /// <summary>
  34. /// Amount of space to leave on bottom of control. Graph content (<see cref="Series"/>)
  35. /// will not be rendered in margins but axis labels may be
  36. /// </summary>
  37. public uint MarginBottom { get; set; }
  38. /// <summary>
  39. /// The graph space position of the bottom left of the control.
  40. /// Changing this scrolls the viewport around in the graph
  41. /// </summary>
  42. /// <value></value>
  43. public PointF ScrollOffset { get; set; } = new PointF (0, 0);
  44. /// <summary>
  45. /// Translates console width/height into graph space. Defaults
  46. /// to 1 row/col of console space being 1 unit of graph space.
  47. /// </summary>
  48. /// <returns></returns>
  49. public PointF CellSize { get; set; } = new PointF (1, 1);
  50. /// <summary>
  51. /// The color of the background of the graph and axis/labels
  52. /// </summary>
  53. public Attribute? GraphColor { get; set; }
  54. /// <summary>
  55. /// Creates a new graph with a 1 to 1 graph space with absolute layout
  56. /// </summary>
  57. public GraphView ()
  58. {
  59. CanFocus = true;
  60. AxisX = new HorizontalAxis ();
  61. AxisY = new VerticalAxis ();
  62. // Things this view knows how to do
  63. AddCommand (Command.ScrollUp, () => { Scroll (0, CellSize.Y); return true; });
  64. AddCommand (Command.ScrollDown, () => { Scroll (0, -CellSize.Y); return true; });
  65. AddCommand (Command.ScrollRight, () => { Scroll (CellSize.X, 0); return true; });
  66. AddCommand (Command.ScrollLeft, () => { Scroll (-CellSize.X, 0); return true; });
  67. AddCommand (Command.PageUp, () => { PageUp (); return true; });
  68. AddCommand (Command.PageDown, () => { PageDown (); return true; });
  69. AddKeyBinding (Key.CursorRight, Command.ScrollRight);
  70. AddKeyBinding (Key.CursorLeft, Command.ScrollLeft);
  71. AddKeyBinding (Key.CursorUp, Command.ScrollUp);
  72. AddKeyBinding (Key.CursorDown, Command.ScrollDown);
  73. // Not bound by default (preserves backwards compatibility)
  74. //AddKeyBinding (Key.PageUp, Command.PageUp);
  75. //AddKeyBinding (Key.PageDown, Command.PageDown);
  76. }
  77. /// <summary>
  78. /// Clears all settings configured on the graph and resets all properties
  79. /// to default values (<see cref="CellSize"/>, <see cref="ScrollOffset"/> etc)
  80. /// </summary>
  81. public void Reset ()
  82. {
  83. ScrollOffset = new PointF (0, 0);
  84. CellSize = new PointF (1, 1);
  85. AxisX.Reset ();
  86. AxisY.Reset ();
  87. Series.Clear ();
  88. Annotations.Clear ();
  89. GraphColor = null;
  90. SetNeedsDisplay ();
  91. }
  92. ///<inheritdoc/>
  93. public override void OnDrawContent (Rect contentArea)
  94. {
  95. if (CellSize.X == 0 || CellSize.Y == 0) {
  96. throw new Exception ($"{nameof (CellSize)} cannot be 0");
  97. }
  98. SetDriverColorToGraphColor ();
  99. Move (0, 0);
  100. // clear all old content
  101. for (int i = 0; i < Bounds.Height; i++) {
  102. Move (0, i);
  103. Driver.AddStr (new string (' ', Bounds.Width));
  104. }
  105. // If there is no data do not display a graph
  106. if (!Series.Any () && !Annotations.Any ()) {
  107. return;
  108. }
  109. // The drawable area of the graph (anything that isn't in the margins)
  110. var graphScreenWidth = Bounds.Width - ((int)MarginLeft);
  111. var graphScreenHeight = Bounds.Height - (int)MarginBottom;
  112. // if the margins take up the full draw bounds don't render
  113. if (graphScreenWidth < 0 || graphScreenHeight < 0) {
  114. return;
  115. }
  116. // Draw 'before' annotations
  117. foreach (var a in Annotations.ToArray ().Where (a => a.BeforeSeries)) {
  118. a.Render (this);
  119. }
  120. SetDriverColorToGraphColor ();
  121. AxisY.DrawAxisLine (this);
  122. AxisX.DrawAxisLine (this);
  123. AxisY.DrawAxisLabels (this);
  124. AxisX.DrawAxisLabels (this);
  125. // Draw a cross where the two axis cross
  126. var axisIntersection = new Point (AxisY.GetAxisXPosition (this), AxisX.GetAxisYPosition (this));
  127. if (AxisX.Visible && AxisY.Visible) {
  128. Move (axisIntersection.X, axisIntersection.Y);
  129. AddRune (axisIntersection.X, axisIntersection.Y, (Rune)'\u253C');
  130. }
  131. SetDriverColorToGraphColor ();
  132. Rect drawBounds = new Rect ((int)MarginLeft, 0, graphScreenWidth, graphScreenHeight);
  133. RectangleF graphSpace = ScreenToGraphSpace (drawBounds);
  134. foreach (var s in Series.ToArray ()) {
  135. s.DrawSeries (this, drawBounds, graphSpace);
  136. // If a series changes the graph color reset it
  137. SetDriverColorToGraphColor ();
  138. }
  139. SetDriverColorToGraphColor ();
  140. // Draw 'after' annotations
  141. foreach (var a in Annotations.ToArray ().Where (a => !a.BeforeSeries)) {
  142. a.Render (this);
  143. }
  144. }
  145. /// <summary>
  146. /// Sets the color attribute of <see cref="Application.Driver"/> to the <see cref="GraphColor"/>
  147. /// (if defined) or <see cref="ColorScheme"/> otherwise.
  148. /// </summary>
  149. public void SetDriverColorToGraphColor ()
  150. {
  151. Driver.SetAttribute (GraphColor ?? (GetNormalColor ()));
  152. }
  153. /// <summary>
  154. /// Returns the section of the graph that is represented by the given
  155. /// screen position
  156. /// </summary>
  157. /// <param name="col"></param>
  158. /// <param name="row"></param>
  159. /// <returns></returns>
  160. public RectangleF ScreenToGraphSpace (int col, int row)
  161. {
  162. return new RectangleF (
  163. ScrollOffset.X + ((col - MarginLeft) * CellSize.X),
  164. ScrollOffset.Y + ((Bounds.Height - (row + MarginBottom + 1)) * CellSize.Y),
  165. CellSize.X, CellSize.Y);
  166. }
  167. /// <summary>
  168. /// Returns the section of the graph that is represented by the screen area
  169. /// </summary>
  170. /// <param name="screenArea"></param>
  171. /// <returns></returns>
  172. public RectangleF ScreenToGraphSpace (Rect screenArea)
  173. {
  174. // get position of the bottom left
  175. var pos = ScreenToGraphSpace (screenArea.Left, screenArea.Bottom - 1);
  176. return new RectangleF (pos.X, pos.Y, screenArea.Width * CellSize.X, screenArea.Height * CellSize.Y);
  177. }
  178. /// <summary>
  179. /// Calculates the screen location for a given point in graph space.
  180. /// Bear in mind these be off screen
  181. /// </summary>
  182. /// <param name="location">Point in graph space that may or may not be represented in the
  183. /// visible area of graph currently presented. E.g. 0,0 for origin</param>
  184. /// <returns>Screen position (Column/Row) which would be used to render the graph <paramref name="location"/>.
  185. /// Note that this can be outside the current client area of the control</returns>
  186. public Point GraphSpaceToScreen (PointF location)
  187. {
  188. return new Point (
  189. (int)((location.X - ScrollOffset.X) / CellSize.X) + (int)MarginLeft,
  190. // screen coordinates are top down while graph coordinates are bottom up
  191. (Bounds.Height - 1) - (int)MarginBottom - (int)((location.Y - ScrollOffset.Y) / CellSize.Y)
  192. );
  193. }
  194. /// <inheritdoc/>
  195. /// <remarks>Also ensures that cursor is invisible after entering the <see cref="GraphView"/>.</remarks>
  196. public override bool OnEnter (View view)
  197. {
  198. Driver.SetCursorVisibility (CursorVisibility.Invisible);
  199. return base.OnEnter (view);
  200. }
  201. /// <inheritdoc/>
  202. public override bool ProcessKey (KeyEvent keyEvent)
  203. {
  204. if (HasFocus && CanFocus) {
  205. var result = InvokeKeybindings (keyEvent);
  206. if (result != null)
  207. return (bool)result;
  208. }
  209. return base.ProcessKey (keyEvent);
  210. }
  211. /// <summary>
  212. /// Scrolls the graph up 1 page
  213. /// </summary>
  214. public void PageUp ()
  215. {
  216. Scroll (0, CellSize.Y * Bounds.Height);
  217. }
  218. /// <summary>
  219. /// Scrolls the graph down 1 page
  220. /// </summary>
  221. public void PageDown ()
  222. {
  223. Scroll (0, -1 * CellSize.Y * Bounds.Height);
  224. }
  225. /// <summary>
  226. /// Scrolls the view by a given number of units in graph space.
  227. /// See <see cref="CellSize"/> to translate this into rows/cols
  228. /// </summary>
  229. /// <param name="offsetX"></param>
  230. /// <param name="offsetY"></param>
  231. public void Scroll (float offsetX, float offsetY)
  232. {
  233. ScrollOffset = new PointF (
  234. ScrollOffset.X + offsetX,
  235. ScrollOffset.Y + offsetY);
  236. SetNeedsDisplay ();
  237. }
  238. #region Bresenham's line algorithm
  239. // https://rosettacode.org/wiki/Bitmap/Bresenham%27s_line_algorithm#C.23
  240. int ipart (decimal x) { return (int)x; }
  241. decimal fpart (decimal x)
  242. {
  243. if (x < 0) return (1 - (x - Math.Floor (x)));
  244. return (x - Math.Floor (x));
  245. }
  246. /// <summary>
  247. /// Draws a line between two points in screen space. Can be diagonals.
  248. /// </summary>
  249. /// <param name="start"></param>
  250. /// <param name="end"></param>
  251. /// <param name="symbol">The symbol to use for the line</param>
  252. public void DrawLine (Point start, Point end, Rune symbol)
  253. {
  254. if (Equals (start, end)) {
  255. return;
  256. }
  257. int x0 = start.X;
  258. int y0 = start.Y;
  259. int x1 = end.X;
  260. int y1 = end.Y;
  261. int dx = Math.Abs (x1 - x0), sx = x0 < x1 ? 1 : -1;
  262. int dy = Math.Abs (y1 - y0), sy = y0 < y1 ? 1 : -1;
  263. int err = (dx > dy ? dx : -dy) / 2, e2;
  264. while (true) {
  265. AddRune (x0, y0, symbol);
  266. if (x0 == x1 && y0 == y1) break;
  267. e2 = err;
  268. if (e2 > -dx) { err -= dy; x0 += sx; }
  269. if (e2 < dy) { err += dx; y0 += sy; }
  270. }
  271. }
  272. #endregion
  273. }
  274. }