Axis.cs 16 KB

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