Axis.cs 16 KB

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