using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; namespace Terminal.Gui { /// /// Describes a series of data that can be rendered into a > /// public interface ISeries { /// /// Draws the section of a series into the /// view /// /// Graph series is to be drawn onto /// Visible area of the graph in Console Screen units (excluding margins) /// Visible area of the graph in Graph space units void DrawSeries (GraphView graph, Rect drawBounds, RectangleF graphBounds); } /// /// Series composed of any number of discrete data points /// public class ScatterSeries : ISeries { /// /// Collection of each discrete point in the series /// /// public List Points { get; set; } = new List (); /// /// The color and character that will be rendered in the console /// when there are point(s) in the corresponding graph space. /// Defaults to uncolored 'x' /// public GraphCellToRender Fill { get; set; } = new GraphCellToRender ('x'); /// /// Draws all points directly onto the graph /// public void DrawSeries (GraphView graph, Rect drawBounds, RectangleF graphBounds) { if (Fill.Color.HasValue) { Application.Driver.SetAttribute (Fill.Color.Value); } foreach (var p in Points.Where (p => graphBounds.Contains (p))) { var screenPoint = graph.GraphSpaceToScreen (p); graph.AddRune (screenPoint.X, screenPoint.Y, Fill.Rune); } } } /// /// Collection of in which bars are clustered by category /// public class MultiBarSeries : ISeries { BarSeries [] subSeries; /// /// Sub collections. Each series contains the bars for a different category. Thus /// SubSeries[0].Bars[0] is the first bar on the axis and SubSeries[1].Bars[0] is the /// second etc /// public IReadOnlyCollection SubSeries { get => new ReadOnlyCollection (subSeries); } /// /// The number of units of graph space between bars. Should be /// less than /// public float Spacing { get; } /// /// Creates a new series of clustered bars. /// /// Each category has this many bars /// How far appart to put each category (in graph space) /// How much spacing between bars in a category (should be less than /) /// Array of colors that define bar color in each category. Length must match public MultiBarSeries (int numberOfBarsPerCategory, float barsEvery, float spacing, Attribute [] colors = null) { subSeries = new BarSeries [numberOfBarsPerCategory]; if (colors != null && colors.Length != numberOfBarsPerCategory) { throw new ArgumentException ("Number of colors must match the number of bars", nameof (numberOfBarsPerCategory)); } for (int i = 0; i < numberOfBarsPerCategory; i++) { subSeries [i] = new BarSeries (); subSeries [i].BarEvery = barsEvery; subSeries [i].Offset = i * spacing; // Only draw labels for the first bar in each category subSeries [i].DrawLabels = i == 0; if (colors != null) { subSeries [i].OverrideBarColor = colors [i]; } } Spacing = spacing; } /// /// Adds a new cluster of bars /// /// /// /// Values for each bar in category, must match the number of bars per category public void AddBars (string label, Rune fill, params float [] values) { if (values.Length != subSeries.Length) { throw new ArgumentException ("Number of values must match the number of bars per category", nameof (values)); } for (int i = 0; i < values.Length; i++) { subSeries [i].Bars.Add (new BarSeries.Bar (label, new GraphCellToRender (fill), values [i])); } } /// /// Draws all /// /// /// /// public void DrawSeries (GraphView graph, Rect drawBounds, RectangleF graphBounds) { foreach (var bar in subSeries) { bar.DrawSeries (graph, drawBounds, graphBounds); } } } /// /// Series of bars positioned at regular intervals /// public class BarSeries : ISeries { /// /// Ordered collection of graph bars to position along axis /// public List Bars { get; set; } = new List (); /// /// Determines the spacing of bars along the axis. Defaults to 1 i.e. /// every 1 unit of graph space a bar is rendered. Note that you should /// also consider when changing this. /// public float BarEvery { get; set; } = 1; /// /// Direction bars protrude from the corresponding axis. /// Defaults to vertical /// public Orientation Orientation { get; set; } = Orientation.Vertical; /// /// The number of units of graph space along the axis before rendering the first bar /// (and subsequent bars - see ). Defaults to 0 /// public float Offset { get; set; } = 0; /// /// Overrides the with a fixed color /// public Attribute? OverrideBarColor { get; set; } /// /// True to draw along the axis under the bar. Defaults /// to true. /// public bool DrawLabels { get; set; } = true; /// /// Applies any color overriding /// /// /// protected virtual GraphCellToRender AdjustColor (GraphCellToRender graphCellToRender) { if (OverrideBarColor.HasValue) { graphCellToRender.Color = OverrideBarColor; } return graphCellToRender; } /// /// Draws bars that are currently in the /// /// /// Screen area of the graph excluding margins /// Graph space area that should be drawn into public virtual void DrawSeries (GraphView graph, Rect drawBounds, RectangleF graphBounds) { for (int i = 0; i < Bars.Count; i++) { float xStart = Orientation == Orientation.Horizontal ? 0 : Offset + ((i + 1) * BarEvery); float yStart = Orientation == Orientation.Horizontal ? Offset + ((i + 1) * BarEvery) : 0; float endX = Orientation == Orientation.Horizontal ? Bars [i].Value : xStart; float endY = Orientation == Orientation.Horizontal ? yStart : Bars [i].Value; // translate to screen positions var screenStart = graph.GraphSpaceToScreen (new PointF (xStart, yStart)); var screenEnd = graph.GraphSpaceToScreen (new PointF (endX, endY)); // Start the bar from wherever the axis is if (Orientation == Orientation.Horizontal) { screenStart.X = graph.AxisY.GetAxisXPosition (graph); // dont draw bar off the right of the control screenEnd.X = Math.Min (graph.Bounds.Width - 1, screenEnd.X); // if bar is off the screen if (screenStart.Y < 0 || screenStart.Y > drawBounds.Height - graph.MarginBottom) { continue; } } else { // Start the axis screenStart.Y = graph.AxisX.GetAxisYPosition (graph); // dont draw bar up above top of control screenEnd.Y = Math.Max (0, screenEnd.Y); // if bar is off the screen if (screenStart.X < graph.MarginLeft || screenStart.X > graph.MarginLeft + drawBounds.Width - 1) { continue; } } // draw the bar unless it has no height if (Bars [i].Value != 0) { DrawBarLine (graph, screenStart, screenEnd, Bars [i]); } // If we are drawing labels and the bar has one if (DrawLabels && !string.IsNullOrWhiteSpace (Bars [i].Text)) { // Add the label to the relevant axis if (Orientation == Orientation.Horizontal) { graph.AxisY.DrawAxisLabel (graph, screenStart.Y, Bars [i].Text); } else if (Orientation == Orientation.Vertical) { graph.AxisX.DrawAxisLabel (graph, screenStart.X, Bars [i].Text); } } } } /// /// Override to do custom drawing of the bar e.g. to apply varying color or changing the fill /// symbol mid bar. /// /// /// Screen position of the start of the bar /// Screen position of the end of the bar /// The Bar that occupies this space and is being drawn protected virtual void DrawBarLine (GraphView graph, Point start, Point end, Bar beingDrawn) { var adjusted = AdjustColor (beingDrawn.Fill); if (adjusted.Color.HasValue) { Application.Driver.SetAttribute (adjusted.Color.Value); } graph.DrawLine (start, end, adjusted.Rune); graph.SetDriverColorToGraphColor (); } /// /// A single bar in a /// public class Bar { /// /// Optional text that describes the bar. This will be rendered on the corresponding /// unless is false /// public string Text { get; set; } /// /// The color and character that will be rendered in the console /// when the bar extends over it /// public GraphCellToRender Fill { get; set; } /// /// The value in graph space X/Y (depending on ) to which the bar extends. /// public float Value { get; } /// /// Creates a new instance of a single bar rendered in the given that extends /// out graph space units in the default /// /// /// /// public Bar (string text, GraphCellToRender fill, float value) { Text = text; Fill = fill; Value = value; } } } }