Series.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. using System.Collections.ObjectModel;
  2. namespace Terminal.Gui;
  3. #nullable enable
  4. /// <summary>Describes a series of data that can be rendered into a <see cref="GraphView"/>></summary>
  5. public interface ISeries
  6. {
  7. /// <summary>
  8. /// Draws the <paramref name="graphBounds"/> section of a series into the <paramref name="graph"/> view
  9. /// <paramref name="drawBounds"/>
  10. /// </summary>
  11. /// <param name="graph">Graph series is to be drawn onto</param>
  12. /// <param name="drawBounds">Visible area of the graph in Console Screen units (excluding margins)</param>
  13. /// <param name="graphBounds">Visible area of the graph in Graph space units</param>
  14. void DrawSeries (GraphView graph, Rectangle drawBounds, RectangleF graphBounds);
  15. }
  16. /// <summary>Series composed of any number of discrete data points</summary>
  17. public class ScatterSeries : ISeries
  18. {
  19. /// <summary>
  20. /// The color and character that will be rendered in the console when there are point(s) in the corresponding
  21. /// graph space. Defaults to uncolored 'dot'
  22. /// </summary>
  23. public GraphCellToRender Fill { get; set; } = new (Glyphs.Dot);
  24. /// <summary>Collection of each discrete point in the series</summary>
  25. /// <returns></returns>
  26. public List<PointF> Points { get; set; } = new ();
  27. /// <summary>Draws all points directly onto the graph</summary>
  28. public void DrawSeries (GraphView graph, Rectangle drawBounds, RectangleF graphBounds)
  29. {
  30. if (Fill.Color.HasValue)
  31. {
  32. graph.SetAttribute (Fill.Color.Value);
  33. }
  34. foreach (PointF p in Points.Where (p => graphBounds.Contains (p)))
  35. {
  36. Point screenPoint = graph.GraphSpaceToScreen (p);
  37. graph.AddRune (screenPoint.X, screenPoint.Y, Fill.Rune);
  38. }
  39. }
  40. }
  41. /// <summary>Collection of <see cref="BarSeries"/> in which bars are clustered by category</summary>
  42. public class MultiBarSeries : ISeries
  43. {
  44. private readonly BarSeries [] subSeries;
  45. /// <summary>Creates a new series of clustered bars.</summary>
  46. /// <param name="numberOfBarsPerCategory">Each category has this many bars</param>
  47. /// <param name="barsEvery">How far apart to put each category (in graph space)</param>
  48. /// <param name="spacing">
  49. /// How much spacing between bars in a category (should be less than <paramref name="barsEvery"/>/
  50. /// <paramref name="numberOfBarsPerCategory"/>)
  51. /// </param>
  52. /// <param name="colors">
  53. /// Array of colors that define bar color in each category. Length must match
  54. /// <paramref name="numberOfBarsPerCategory"/>
  55. /// </param>
  56. public MultiBarSeries (int numberOfBarsPerCategory, float barsEvery, float spacing, Attribute []? colors = null)
  57. {
  58. subSeries = new BarSeries [numberOfBarsPerCategory];
  59. if (colors is { } && colors.Length != numberOfBarsPerCategory)
  60. {
  61. throw new ArgumentException (
  62. "Number of colors must match the number of bars",
  63. nameof (numberOfBarsPerCategory)
  64. );
  65. }
  66. for (var i = 0; i < numberOfBarsPerCategory; i++)
  67. {
  68. subSeries [i] = new BarSeries ();
  69. subSeries [i].BarEvery = barsEvery;
  70. subSeries [i].Offset = i * spacing;
  71. // Only draw labels for the first bar in each category
  72. subSeries [i].DrawLabels = i == 0;
  73. if (colors is { })
  74. {
  75. subSeries [i].OverrideBarColor = colors [i];
  76. }
  77. }
  78. Spacing = spacing;
  79. }
  80. /// <summary>
  81. /// The number of units of graph space between bars. Should be less than <see cref="BarSeries.BarEvery"/>
  82. /// </summary>
  83. public float Spacing { get; }
  84. /// <summary>
  85. /// Sub collections. Each series contains the bars for a different category. Thus SubSeries[0].Bars[0] is the
  86. /// first bar on the axis and SubSeries[1].Bars[0] is the second etc.
  87. /// </summary>
  88. public IReadOnlyCollection<BarSeries> SubSeries => new ReadOnlyCollection<BarSeries> (subSeries);
  89. /// <summary>Draws all <see cref="SubSeries"/></summary>
  90. /// <param name="graph"></param>
  91. /// <param name="drawBounds"></param>
  92. /// <param name="graphBounds"></param>
  93. public void DrawSeries (GraphView graph, Rectangle drawBounds, RectangleF graphBounds)
  94. {
  95. foreach (BarSeries bar in subSeries)
  96. {
  97. bar.DrawSeries (graph, drawBounds, graphBounds);
  98. }
  99. }
  100. /// <summary>Adds a new cluster of bars</summary>
  101. /// <param name="label"></param>
  102. /// <param name="fill"></param>
  103. /// <param name="values">Values for each bar in category, must match the number of bars per category</param>
  104. public void AddBars (string label, Rune fill, params float [] values)
  105. {
  106. if (values.Length != subSeries.Length)
  107. {
  108. throw new ArgumentException (
  109. "Number of values must match the number of bars per category",
  110. nameof (values)
  111. );
  112. }
  113. for (var i = 0; i < values.Length; i++)
  114. {
  115. subSeries [i]
  116. .Bars.Add (
  117. new BarSeriesBar (
  118. label,
  119. new GraphCellToRender (fill),
  120. values [i]
  121. )
  122. );
  123. }
  124. }
  125. }
  126. /// <summary>Series of bars positioned at regular intervals</summary>
  127. public class BarSeries : ISeries
  128. {
  129. /// <summary>
  130. /// Determines the spacing of bars along the axis. Defaults to 1 i.e. every 1 unit of graph space a bar is
  131. /// rendered. Note that you should also consider <see cref="GraphView.CellSize"/> when changing this.
  132. /// </summary>
  133. public float BarEvery { get; set; } = 1;
  134. /// <summary>Ordered collection of graph bars to position along axis</summary>
  135. public List<BarSeriesBar> Bars { get; set; } = new ();
  136. /// <summary>True to draw <see cref="BarSeriesBar.Text"/> along the axis under the bar. Defaults to true.</summary>
  137. public bool DrawLabels { get; set; } = true;
  138. /// <summary>
  139. /// The number of units of graph space along the axis before rendering the first bar (and subsequent bars - see
  140. /// <see cref="BarEvery"/>). Defaults to 0
  141. /// </summary>
  142. public float Offset { get; set; }
  143. /// <summary>Direction bars protrude from the corresponding axis. Defaults to vertical</summary>
  144. public Orientation Orientation { get; set; } = Orientation.Vertical;
  145. /// <summary>Overrides the <see cref="BarSeriesBar.Fill"/> with a fixed color</summary>
  146. public Attribute? OverrideBarColor { get; set; }
  147. /// <summary>Draws bars that are currently in the <paramref name="drawBounds"/></summary>
  148. /// <param name="graph"></param>
  149. /// <param name="drawBounds">Screen area of the graph excluding margins</param>
  150. /// <param name="graphBounds">Graph space area that should be drawn into <paramref name="drawBounds"/></param>
  151. public virtual void DrawSeries (GraphView graph, Rectangle drawBounds, RectangleF graphBounds)
  152. {
  153. for (var i = 0; i < Bars.Count; i++)
  154. {
  155. float xStart = Orientation == Orientation.Horizontal ? 0 : Offset + (i + 1) * BarEvery;
  156. float yStart = Orientation == Orientation.Horizontal ? Offset + (i + 1) * BarEvery : 0;
  157. float endX = Orientation == Orientation.Horizontal ? Bars [i].Value : xStart;
  158. float endY = Orientation == Orientation.Horizontal ? yStart : Bars [i].Value;
  159. // translate to screen positions
  160. Point screenStart = graph.GraphSpaceToScreen (new PointF (xStart, yStart));
  161. Point screenEnd = graph.GraphSpaceToScreen (new PointF (endX, endY));
  162. // Start the bar from wherever the axis is
  163. if (Orientation == Orientation.Horizontal)
  164. {
  165. screenStart.X = graph.AxisY.GetAxisXPosition (graph);
  166. // don't draw bar off the right of the control
  167. screenEnd.X = Math.Min (graph.Viewport.Width - 1, screenEnd.X);
  168. // if bar is off the screen
  169. if (screenStart.Y < 0 || screenStart.Y > drawBounds.Height - graph.MarginBottom)
  170. {
  171. continue;
  172. }
  173. }
  174. else
  175. {
  176. // Start the axis
  177. screenStart.Y = graph.AxisX.GetAxisYPosition (graph);
  178. // don't draw bar up above top of control
  179. screenEnd.Y = Math.Max (0, screenEnd.Y);
  180. // if bar is off the screen
  181. if (screenStart.X < graph.MarginLeft || screenStart.X > graph.MarginLeft + drawBounds.Width - 1)
  182. {
  183. continue;
  184. }
  185. }
  186. // draw the bar unless it has no height
  187. if (Bars [i].Value != 0)
  188. {
  189. DrawBarLine (graph, screenStart, screenEnd, Bars [i]);
  190. }
  191. // If we are drawing labels and the bar has one
  192. if (DrawLabels && !string.IsNullOrWhiteSpace (Bars [i].Text))
  193. {
  194. // Add the label to the relevant axis
  195. if (Orientation == Orientation.Horizontal)
  196. {
  197. graph.AxisY.DrawAxisLabel (graph, screenStart.Y, Bars [i].Text);
  198. }
  199. else if (Orientation == Orientation.Vertical)
  200. {
  201. graph.AxisX.DrawAxisLabel (graph, screenStart.X, Bars [i].Text);
  202. }
  203. }
  204. }
  205. }
  206. /// <summary>Applies any color overriding</summary>
  207. /// <param name="graphCellToRender"></param>
  208. /// <returns></returns>
  209. protected virtual GraphCellToRender AdjustColor (GraphCellToRender graphCellToRender)
  210. {
  211. if (OverrideBarColor.HasValue)
  212. {
  213. graphCellToRender.Color = OverrideBarColor;
  214. }
  215. return graphCellToRender;
  216. }
  217. /// <summary>Override to do custom drawing of the bar e.g. to apply varying color or changing the fill symbol mid bar.</summary>
  218. /// <param name="graph"></param>
  219. /// <param name="start">Screen position of the start of the bar</param>
  220. /// <param name="end">Screen position of the end of the bar</param>
  221. /// <param name="beingDrawn">The Bar that occupies this space and is being drawn</param>
  222. protected virtual void DrawBarLine (GraphView graph, Point start, Point end, BarSeriesBar beingDrawn)
  223. {
  224. GraphCellToRender adjusted = AdjustColor (beingDrawn.Fill);
  225. if (adjusted.Color.HasValue)
  226. {
  227. graph.SetAttribute (adjusted.Color.Value);
  228. }
  229. graph.DrawLine (start, end, adjusted.Rune);
  230. graph.SetDriverColorToGraphColor ();
  231. }
  232. }