Axis.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. #nullable disable
  2. 
  3. namespace Terminal.Gui.Views;
  4. /// <summary>Renders a continuous line with grid line ticks and labels</summary>
  5. public abstract class Axis
  6. {
  7. /// <summary>Default value for <see cref="ShowLabelsEvery"/></summary>
  8. private const uint DefaultShowLabelsEvery = 5;
  9. /// <summary>
  10. /// Allows you to control what label text is rendered for a given <see cref="Increment"/> when
  11. /// <see cref="ShowLabelsEvery"/> is above 0
  12. /// </summary>
  13. public LabelGetterDelegate LabelGetter;
  14. /// <summary>Populates base properties and sets the read only <see cref="Orientation"/></summary>
  15. /// <param name="orientation"></param>
  16. protected Axis (Orientation orientation)
  17. {
  18. Orientation = orientation;
  19. LabelGetter = DefaultLabelGetter;
  20. }
  21. /// <summary>Number of units of graph space between ticks on axis. 0 for no ticks</summary>
  22. /// <value></value>
  23. public float Increment { get; set; } = 1;
  24. /// <summary>The minimum axis point to show. Defaults to null (no minimum)</summary>
  25. public float? Minimum { get; set; }
  26. /// <summary>Direction of the axis</summary>
  27. /// <value></value>
  28. public Orientation Orientation { get; }
  29. /// <summary>The number of <see cref="Increment"/> before an label is added. 0 = never show labels</summary>
  30. public uint ShowLabelsEvery { get; set; } = DefaultShowLabelsEvery;
  31. /// <summary>
  32. /// Displayed below/to left of labels (see <see cref="Orientation"/>). If text is not visible, check
  33. /// <see cref="GraphView.MarginBottom"/> / <see cref="GraphView.MarginLeft"/>
  34. /// </summary>
  35. public string Text { get; set; }
  36. /// <summary>True to render axis. Defaults to true</summary>
  37. public bool Visible { get; set; } = true;
  38. /// <summary>
  39. /// Draws a custom label <paramref name="text"/> at <paramref name="screenPosition"/> units along the axis (X or Y
  40. /// depending on <see cref="Orientation"/>)
  41. /// </summary>
  42. /// <param name="graph"></param>
  43. /// <param name="screenPosition"></param>
  44. /// <param name="text"></param>
  45. public abstract void DrawAxisLabel (GraphView graph, int screenPosition, string text);
  46. /// <summary>Draws labels and axis <see cref="Increment"/> ticks</summary>
  47. /// <param name="graph"></param>
  48. public abstract void DrawAxisLabels (GraphView graph);
  49. /// <summary>Draws the solid line of the axis</summary>
  50. /// <param name="graph"></param>
  51. public abstract void DrawAxisLine (GraphView graph);
  52. /// <summary>Resets all configurable properties of the axis to default values</summary>
  53. public virtual void Reset ()
  54. {
  55. Increment = 1;
  56. ShowLabelsEvery = DefaultShowLabelsEvery;
  57. Visible = true;
  58. Text = "";
  59. LabelGetter = DefaultLabelGetter;
  60. Minimum = null;
  61. }
  62. /// <summary>Draws a single cell of the solid line of the axis</summary>
  63. /// <param name="graph"></param>
  64. /// <param name="x"></param>
  65. /// <param name="y"></param>
  66. protected abstract void DrawAxisLine (GraphView graph, int x, int y);
  67. private string DefaultLabelGetter (AxisIncrementToRender toRender) { return toRender.Value.ToString ("N0"); }
  68. }
  69. /// <summary>The horizontal (x-axis) of a <see cref="GraphView"/></summary>
  70. public class HorizontalAxis : Axis
  71. {
  72. /// <summary>
  73. /// Creates a new instance of axis with an <see cref="Orientation"/> of <see cref="Orientation.Horizontal"/>
  74. /// </summary>
  75. public HorizontalAxis () : base (Orientation.Horizontal) { }
  76. /// <summary>
  77. /// Draws the given <paramref name="text"/> on the axis at x <paramref name="screenPosition"/>. For the screen y
  78. /// position use <see cref="GetAxisYPosition(GraphView)"/>
  79. /// </summary>
  80. /// <param name="graph">Graph being drawn onto</param>
  81. /// <param name="screenPosition">Number of screen columns along the axis to take before rendering</param>
  82. /// <param name="text">Text to render under the axis tick</param>
  83. public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
  84. {
  85. IDriver driver = Application.Driver;
  86. int y = GetAxisYPosition (graph);
  87. graph.Move (screenPosition, y);
  88. // draw the tick on the axis
  89. Application.Driver?.AddRune (Glyphs.TopTee);
  90. // and the label text
  91. if (!string.IsNullOrWhiteSpace (text))
  92. {
  93. // center the label but don't draw it outside bounds of the graph
  94. int drawAtX = Math.Max (0, screenPosition - text.Length / 2);
  95. string toRender = text;
  96. // this is how much space is left
  97. int xSpaceAvailable = graph.Viewport.Width - drawAtX;
  98. // There is no space for the label at all!
  99. if (xSpaceAvailable <= 0)
  100. {
  101. return;
  102. }
  103. // if we are close to right side of graph, don't overspill
  104. if (toRender.Length > xSpaceAvailable)
  105. {
  106. toRender = toRender.Substring (0, xSpaceAvailable);
  107. }
  108. graph.Move (drawAtX, Math.Min (y + 1, graph.Viewport.Height - 1));
  109. driver.AddStr (toRender);
  110. }
  111. }
  112. /// <summary>Draws the horizontal x-axis labels and <see cref="Axis.Increment"/> ticks</summary>
  113. public override void DrawAxisLabels (GraphView graph)
  114. {
  115. if (!Visible || Increment == 0)
  116. {
  117. return;
  118. }
  119. Rectangle viewport = graph.Viewport;
  120. IEnumerable<AxisIncrementToRender> labels = GetLabels (graph, viewport);
  121. foreach (AxisIncrementToRender label in labels)
  122. {
  123. DrawAxisLabel (graph, label.ScreenLocation, label.Text);
  124. }
  125. // if there is a title
  126. if (!string.IsNullOrWhiteSpace (Text))
  127. {
  128. string toRender = Text;
  129. // if label is too long
  130. if (toRender.Length > graph.Viewport.Width)
  131. {
  132. toRender = toRender.Substring (0, graph.Viewport.Width);
  133. }
  134. graph.Move (graph.Viewport.Width / 2 - toRender.Length / 2, graph.Viewport.Height - 1);
  135. Application.Driver?.AddStr (toRender);
  136. }
  137. }
  138. /// <summary>Draws the horizontal axis line</summary>
  139. /// <param name="graph"></param>
  140. public override void DrawAxisLine (GraphView graph)
  141. {
  142. if (!Visible)
  143. {
  144. return;
  145. }
  146. Rectangle bounds = graph.Viewport;
  147. graph.Move (0, 0);
  148. int y = GetAxisYPosition (graph);
  149. // start the x-axis at left of screen (either 0 or margin)
  150. var xStart = (int)graph.MarginLeft;
  151. // but if the x-axis has a minimum (minimum is in graph space units)
  152. if (Minimum.HasValue)
  153. {
  154. // start at the screen location of the minimum
  155. int minimumScreenX = graph.GraphSpaceToScreen (new PointF (Minimum.Value, y)).X;
  156. // unless that is off the screen to the left
  157. xStart = Math.Max (xStart, minimumScreenX);
  158. }
  159. for (int i = xStart; i < bounds.Width; i++)
  160. {
  161. DrawAxisLine (graph, i, y);
  162. }
  163. }
  164. /// <summary>
  165. /// Returns the Y screen position of the origin (typically 0,0) of graph space. Return value is bounded by the
  166. /// screen i.e. the axis is always rendered even if the origin is offscreen.
  167. /// </summary>
  168. /// <param name="graph"></param>
  169. public int GetAxisYPosition (GraphView graph)
  170. {
  171. // find the origin of the graph in screen space (this allows for 'crosshair' style
  172. // graphs where positive and negative numbers visible
  173. Point origin = graph.GraphSpaceToScreen (new PointF (0, 0));
  174. // float the X axis so that it accurately represents the origin of the graph
  175. // but anchor it to top/bottom if the origin is offscreen
  176. return Math.Min (Math.Max (0, origin.Y), graph.Viewport.Height - ((int)graph.MarginBottom + 1));
  177. }
  178. /// <summary>Draws a horizontal axis line at the given <paramref name="x"/>, <paramref name="y"/> screen coordinates</summary>
  179. /// <param name="graph"></param>
  180. /// <param name="x"></param>
  181. /// <param name="y"></param>
  182. protected override void DrawAxisLine (GraphView graph, int x, int y)
  183. {
  184. graph.Move (x, y);
  185. Application.Driver?.AddRune (Glyphs.HLine);
  186. }
  187. private IEnumerable<AxisIncrementToRender> GetLabels (GraphView graph, Rectangle viewport)
  188. {
  189. // if no labels
  190. if (Increment == 0)
  191. {
  192. yield break;
  193. }
  194. var labels = 0;
  195. int y = GetAxisYPosition (graph);
  196. RectangleF start = graph.ScreenToGraphSpace ((int)graph.MarginLeft, y);
  197. RectangleF end = graph.ScreenToGraphSpace (viewport.Width, y);
  198. // don't draw labels below the minimum
  199. if (Minimum.HasValue)
  200. {
  201. start.X = Math.Max (start.X, Minimum.Value);
  202. }
  203. RectangleF current = start;
  204. while (current.X < end.X)
  205. {
  206. int screenX = graph.GraphSpaceToScreen (new PointF (current.X, current.Y)).X;
  207. // The increment we will render (normally a top T unicode symbol)
  208. var toRender = new AxisIncrementToRender (Orientation, screenX, current.X);
  209. // Not every increment has to have a label
  210. if (ShowLabelsEvery != 0)
  211. {
  212. // if this increment also needs a label
  213. if (labels++ % ShowLabelsEvery == 0)
  214. {
  215. toRender.Text = LabelGetter (toRender);
  216. }
  217. ;
  218. }
  219. // Label or no label definitely render it
  220. yield return toRender;
  221. current.X += Increment;
  222. }
  223. }
  224. }
  225. /// <summary>The vertical (i.e. Y axis) of a <see cref="GraphView"/></summary>
  226. public class VerticalAxis : Axis
  227. {
  228. /// <summary>Creates a new <see cref="Orientation.Vertical"/> axis</summary>
  229. public VerticalAxis () : base (Orientation.Vertical) { }
  230. /// <summary>
  231. /// Draws the given <paramref name="text"/> on the axis at y <paramref name="screenPosition"/>. For the screen x
  232. /// position use <see cref="GetAxisXPosition(GraphView)"/>
  233. /// </summary>
  234. /// <param name="graph">Graph being drawn onto</param>
  235. /// <param name="screenPosition">Number of rows from the top of the screen (i.e. down the axis) before rendering</param>
  236. /// <param name="text">
  237. /// Text to render to the left of the axis tick. Ensure to set <see cref="GraphView.MarginLeft"/> or
  238. /// <see cref="GraphView.ScrollOffset"/> sufficient that it is visible
  239. /// </param>
  240. public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
  241. {
  242. int x = GetAxisXPosition (graph);
  243. int labelThickness = text.Length;
  244. graph.Move (x, screenPosition);
  245. // draw the tick on the axis
  246. Application.Driver?.AddRune (Glyphs.RightTee);
  247. // and the label text
  248. if (!string.IsNullOrWhiteSpace (text))
  249. {
  250. graph.Move (Math.Max (0, x - labelThickness), screenPosition);
  251. Application.Driver?.AddStr (text);
  252. }
  253. }
  254. /// <summary>Draws axis <see cref="Axis.Increment"/> markers and labels</summary>
  255. /// <param name="graph"></param>
  256. public override void DrawAxisLabels (GraphView graph)
  257. {
  258. if (!Visible || Increment == 0)
  259. {
  260. return;
  261. }
  262. Rectangle bounds = graph.Viewport;
  263. IEnumerable<AxisIncrementToRender> labels = GetLabels (graph, bounds);
  264. foreach (AxisIncrementToRender label in labels)
  265. {
  266. DrawAxisLabel (graph, label.ScreenLocation, label.Text);
  267. }
  268. // if there is a title
  269. if (!string.IsNullOrWhiteSpace (Text))
  270. {
  271. string toRender = Text;
  272. // if label is too long
  273. if (toRender.Length > graph.Viewport.Height)
  274. {
  275. toRender = toRender.Substring (0, graph.Viewport.Height);
  276. }
  277. // Draw it 1 letter at a time vertically down row 0 of the control
  278. int startDrawingAtY = graph.Viewport.Height / 2 - toRender.Length / 2;
  279. for (var i = 0; i < toRender.Length; i++)
  280. {
  281. graph.Move (0, startDrawingAtY + i);
  282. Application.Driver?.AddRune ((Rune)toRender [i]);
  283. }
  284. }
  285. }
  286. /// <summary>Draws the vertical axis line</summary>
  287. /// <param name="graph"></param>
  288. public override void DrawAxisLine (GraphView graph)
  289. {
  290. if (!Visible)
  291. {
  292. return;
  293. }
  294. Rectangle bounds = graph.Viewport;
  295. int x = GetAxisXPosition (graph);
  296. int yEnd = GetAxisYEnd (graph);
  297. // don't draw down further than the control bounds
  298. yEnd = Math.Min (yEnd, bounds.Height - (int)graph.MarginBottom);
  299. // Draw solid line
  300. for (var i = 0; i < yEnd; i++)
  301. {
  302. DrawAxisLine (graph, x, i);
  303. }
  304. }
  305. /// <summary>
  306. /// Returns the X screen position of the origin (typically 0,0) of graph space. Return value is bounded by the
  307. /// screen i.e. the axis is always rendered even if the origin is offscreen.
  308. /// </summary>
  309. /// <param name="graph"></param>
  310. public int GetAxisXPosition (GraphView graph)
  311. {
  312. // find the origin of the graph in screen space (this allows for 'crosshair' style
  313. // graphs where positive and negative numbers visible
  314. Point origin = graph.GraphSpaceToScreen (new PointF (0, 0));
  315. // float the Y axis so that it accurately represents the origin of the graph
  316. // but anchor it to left/right if the origin is offscreen
  317. return Math.Min (Math.Max ((int)graph.MarginLeft, origin.X), graph.Viewport.Width - 1);
  318. }
  319. /// <summary>Draws a vertical axis line at the given <paramref name="x"/>, <paramref name="y"/> screen coordinates</summary>
  320. /// <param name="graph"></param>
  321. /// <param name="x"></param>
  322. /// <param name="y"></param>
  323. protected override void DrawAxisLine (GraphView graph, int x, int y)
  324. {
  325. graph.Move (x, y);
  326. Application.Driver?.AddRune (Glyphs.VLine);
  327. }
  328. private int GetAxisYEnd (GraphView graph)
  329. {
  330. // draw down the screen (0 is top of screen)
  331. // end at the bottom of the screen
  332. //unless there is a minimum
  333. if (Minimum.HasValue)
  334. {
  335. return graph.GraphSpaceToScreen (new PointF (0, Minimum.Value)).Y;
  336. }
  337. return graph.Viewport.Height;
  338. }
  339. private IEnumerable<AxisIncrementToRender> GetLabels (GraphView graph, Rectangle bounds)
  340. {
  341. // if no labels
  342. if (Increment == 0)
  343. {
  344. yield break;
  345. }
  346. var labels = 0;
  347. int x = GetAxisXPosition (graph);
  348. // remember screen space is top down so the lowest graph
  349. // space value is at the bottom of the screen
  350. RectangleF start = graph.ScreenToGraphSpace (x, bounds.Height - (1 + (int)graph.MarginBottom));
  351. RectangleF end = graph.ScreenToGraphSpace (x, 0);
  352. // don't draw labels below the minimum
  353. if (Minimum.HasValue)
  354. {
  355. start.Y = Math.Max (start.Y, Minimum.Value);
  356. }
  357. RectangleF current = start;
  358. while (current.Y < end.Y)
  359. {
  360. int screenY = graph.GraphSpaceToScreen (new PointF (current.X, current.Y)).Y;
  361. // Create the axis symbol
  362. var toRender = new AxisIncrementToRender (Orientation, screenY, current.Y);
  363. // and the label (if we are due one)
  364. if (ShowLabelsEvery != 0)
  365. {
  366. // if this increment also needs a label
  367. if (labels++ % ShowLabelsEvery == 0)
  368. {
  369. toRender.Text = LabelGetter (toRender);
  370. }
  371. ;
  372. }
  373. // draw the axis symbol (and label if it has one)
  374. yield return toRender;
  375. current.Y += Increment;
  376. }
  377. }
  378. }
  379. /// <summary>A location on an axis of a <see cref="GraphView"/> that may or may not have a label associated with it</summary>
  380. public class AxisIncrementToRender
  381. {
  382. private string _text = "";
  383. /// <summary>Describe a new section of an axis that requires an axis increment symbol and/or label</summary>
  384. /// <param name="orientation"></param>
  385. /// <param name="screen"></param>
  386. /// <param name="value"></param>
  387. public AxisIncrementToRender (Orientation orientation, int screen, float value)
  388. {
  389. Orientation = orientation;
  390. ScreenLocation = screen;
  391. Value = value;
  392. }
  393. /// <summary>Direction of the parent axis</summary>
  394. public Orientation Orientation { get; }
  395. /// <summary>The screen location (X or Y depending on <see cref="Orientation"/>) that the increment will be rendered at</summary>
  396. public int ScreenLocation { get; }
  397. /// <summary>The value at this position on the axis in graph space</summary>
  398. public float Value { get; }
  399. /// <summary>The text (if any) that should be displayed at this axis increment</summary>
  400. /// <value></value>
  401. internal string Text
  402. {
  403. get => _text;
  404. set => _text = value ?? "";
  405. }
  406. }
  407. /// <summary>Delegate for custom formatting of axis labels. Determines what should be displayed at a given label</summary>
  408. /// <param name="toRender">The axis increment to which the label is attached</param>
  409. /// <returns></returns>
  410. public delegate string LabelGetterDelegate (AxisIncrementToRender toRender);