Series.cs 11 KB

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