GraphView.cs 8.6 KB

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