Merge branch 'main' of tig:migueldeicaza/gui.cs

This commit is contained in:
Charlie Kindel
2021-04-28 08:39:11 -07:00
15 changed files with 4187 additions and 0 deletions

View File

@@ -26,6 +26,11 @@ namespace Terminal.Gui {
int [,,] contents;
bool [] dirtyLine;
/// <summary>
/// Assists with testing, the format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag
/// </summary>
public int [,,] Contents => contents;
void UpdateOffscreen ()
{
int cols = Cols;

View File

@@ -0,0 +1,310 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Terminal.Gui.Graphs {
/// <summary>
/// <para>Describes an overlay element that is rendered either before or
/// after a series.</para>
///
/// <para>Annotations can be positioned either in screen space (e.g.
/// a legend) or in graph space (e.g. a line showing high point)
/// </para>
/// <para>Unlike <see cref="ISeries"/>, annotations are allowed to
/// draw into graph margins
/// </para>
/// </summary>
public interface IAnnotation {
/// <summary>
/// True if annotation should be drawn before <see cref="ISeries"/>. This
/// allowes Series and later annotations to potentially draw over the top
/// of this annotation.
/// </summary>
bool BeforeSeries { get; }
/// <summary>
/// Called once after series have been rendered (or before if <see cref="BeforeSeries"/> is true).
/// Use <see cref="View.Driver"/> to draw and <see cref="View.Bounds"/> to avoid drawing outside of
/// graph
/// </summary>
/// <param name="graph"></param>
void Render (GraphView graph);
}
/// <summary>
/// Displays text at a given position (in screen space or graph space)
/// </summary>
public class TextAnnotation : IAnnotation {
/// <summary>
/// The location on screen to draw the <see cref="Text"/> regardless
/// of scroll/zoom settings. This overrides <see cref="GraphPosition"/>
/// if specified.
/// </summary>
public Point? ScreenPosition { get; set; }
/// <summary>
/// The location in graph space to draw the <see cref="Text"/>. This
/// annotation will only show if the point is in the current viewable
/// area of the graph presented in the <see cref="GraphView"/>
/// </summary>
public PointF GraphPosition { get; set; }
/// <summary>
/// Text to display on the graph
/// </summary>
public string Text { get; set; }
/// <summary>
/// True to add text before plotting series. Defaults to false
/// </summary>
public bool BeforeSeries { get; set; }
/// <summary>
/// Draws the annotation
/// </summary>
/// <param name="graph"></param>
public void Render (GraphView graph)
{
if (ScreenPosition.HasValue) {
DrawText (graph, ScreenPosition.Value.X, ScreenPosition.Value.Y);
return;
}
var screenPos = graph.GraphSpaceToScreen (GraphPosition);
DrawText (graph, screenPos.X, screenPos.Y);
}
/// <summary>
/// Draws the <see cref="Text"/> at the given coordinates with truncation to avoid
/// spilling over <see name="View.Bounds"/> of the <paramref name="graph"/>
/// </summary>
/// <param name="graph"></param>
/// <param name="x">Screen x position to start drawing string</param>
/// <param name="y">Screen y position to start drawing string</param>
protected void DrawText (GraphView graph, int x, int y)
{
// the draw point is out of control bounds
if (!graph.Bounds.Contains (new Point (x, y))) {
return;
}
// There is no text to draw
if (string.IsNullOrWhiteSpace (Text)) {
return;
}
graph.Move (x, y);
int availableWidth = graph.Bounds.Width - x;
if (availableWidth <= 0) {
return;
}
if (Text.Length < availableWidth) {
View.Driver.AddStr (Text);
} else {
View.Driver.AddStr (Text.Substring (0, availableWidth));
}
}
}
/// <summary>
/// A box containing symbol definitions e.g. meanings for colors in a graph.
/// The 'Key' to the graph
/// </summary>
public class LegendAnnotation : IAnnotation {
/// <summary>
/// True to draw a solid border around the legend.
/// Defaults to true. This border will be within the
/// <see cref="Bounds"/> and so reduces the width/height
/// available for text by 2
/// </summary>
public bool Border { get; set; } = true;
/// <summary>
/// Defines the screen area available for the legend to render in
/// </summary>
public Rect Bounds { get; set; }
/// <summary>
/// Returns false i.e. Lengends render after series
/// </summary>
public bool BeforeSeries => false;
/// <summary>
/// Ordered collection of entries that are rendered in the legend.
/// </summary>
List<Tuple<GraphCellToRender, string>> entries = new List<Tuple<GraphCellToRender, string>> ();
/// <summary>
/// Creates a new empty legend at the given screen coordinates
/// </summary>
/// <param name="legendBounds">Defines the area available for the legend to render in
/// (within the graph). This is in screen units (i.e. not graph space)</param>
public LegendAnnotation (Rect legendBounds)
{
Bounds = legendBounds;
}
/// <summary>
/// Draws the Legend and all entries into the area within <see cref="Bounds"/>
/// </summary>
/// <param name="graph"></param>
public void Render (GraphView graph)
{
if (Border) {
graph.DrawFrame (Bounds, 0, true);
}
// start the legend at
int y = Bounds.Top + (Border ? 1 : 0);
int x = Bounds.Left + (Border ? 1 : 0);
// how much horizontal space is available for writing legend entries?
int availableWidth = Bounds.Width - (Border ? 2 : 0);
int availableHeight = Bounds.Height - (Border ? 2 : 0);
int linesDrawn = 0;
foreach (var entry in entries) {
if (entry.Item1.Color.HasValue) {
Application.Driver.SetAttribute (entry.Item1.Color.Value);
} else {
graph.SetDriverColorToGraphColor ();
}
// add the symbol
graph.AddRune (x, y + linesDrawn, entry.Item1.Rune);
// switch to normal coloring (for the text)
graph.SetDriverColorToGraphColor ();
// add the text
graph.Move (x + 1, y + linesDrawn);
string str = TruncateOrPad (entry.Item2, availableWidth - 1);
Application.Driver.AddStr (str);
linesDrawn++;
// Legend has run out of space
if (linesDrawn >= availableHeight) {
break;
}
}
}
private string TruncateOrPad (string text, int width)
{
if (string.IsNullOrEmpty (text))
return text;
// if value is not wide enough
if (text.Sum (c => Rune.ColumnWidth (c)) < width) {
// pad it out with spaces to the given alignment
int toPad = width - (text.Sum (c => Rune.ColumnWidth (c)));
return text + new string (' ', toPad);
}
// value is too wide
return new string (text.TakeWhile (c => (width -= Rune.ColumnWidth (c)) >= 0).ToArray ());
}
/// <summary>
/// Adds an entry into the legend. Duplicate entries are permissable
/// </summary>
/// <param name="graphCellToRender">The symbol appearing on the graph that should appear in the legend</param>
/// <param name="text">Text to render on this line of the legend. Will be truncated
/// if outside of Legend <see cref="Bounds"/></param>
public void AddEntry (GraphCellToRender graphCellToRender, string text)
{
entries.Add (Tuple.Create (graphCellToRender, text));
}
}
/// <summary>
/// Sequence of lines to connect points e.g. of a <see cref="ScatterSeries"/>
/// </summary>
public class PathAnnotation : IAnnotation {
/// <summary>
/// Points that should be connected. Lines will be drawn between points in the order
/// they appear in the list
/// </summary>
public List<PointF> Points { get; set; } = new List<PointF> ();
/// <summary>
/// Color for the line that connects points
/// </summary>
public Attribute? LineColor { get; set; }
/// <summary>
/// The symbol that gets drawn along the line, defaults to '.'
/// </summary>
public Rune LineRune { get; set; } = new Rune ('.');
/// <summary>
/// True to add line before plotting series. Defaults to false
/// </summary>
public bool BeforeSeries { get; set; }
/// <summary>
/// Draws lines connecting each of the <see cref="Points"/>
/// </summary>
/// <param name="graph"></param>
public void Render (GraphView graph)
{
View.Driver.SetAttribute (LineColor ?? graph.ColorScheme.Normal);
foreach (var line in PointsToLines ()) {
var start = graph.GraphSpaceToScreen (line.Start);
var end = graph.GraphSpaceToScreen (line.End);
graph.DrawLine (start, end, LineRune);
}
}
/// <summary>
/// Generates lines joining <see cref="Points"/>
/// </summary>
/// <returns></returns>
private IEnumerable<LineF> PointsToLines ()
{
for (int i = 0; i < Points.Count - 1; i++) {
yield return new LineF (Points [i], Points [i + 1]);
}
}
/// <summary>
/// Describes two points in graph space and a line between them
/// </summary>
public class LineF {
/// <summary>
/// The start of the line
/// </summary>
public PointF Start { get; }
/// <summary>
/// The end point of the line
/// </summary>
public PointF End { get; }
/// <summary>
/// Creates a new line between the points
/// </summary>
public LineF (PointF start, PointF end)
{
this.Start = start;
this.End = end;
}
}
}
}

View File

@@ -0,0 +1,565 @@
using System;
using System.Collections.Generic;
namespace Terminal.Gui.Graphs {
/// <summary>
/// Renders a continuous line with grid line ticks and labels
/// </summary>
public abstract class Axis {
/// <summary>
/// Default value for <see cref="ShowLabelsEvery"/>
/// </summary>
const uint DefaultShowLabelsEvery = 5;
/// <summary>
/// Direction of the axis
/// </summary>
/// <value></value>
public Orientation Orientation { get; }
/// <summary>
/// Number of units of graph space between ticks on axis. 0 for no ticks
/// </summary>
/// <value></value>
public float Increment { get; set; } = 1;
/// <summary>
/// The number of <see cref="Increment"/> before an label is added.
/// 0 = never show labels
/// </summary>
public uint ShowLabelsEvery { get; set; } = DefaultShowLabelsEvery;
/// <summary>
/// True to render axis. Defaults to true
/// </summary>
public bool Visible { get; set; } = true;
/// <summary>
/// Allows you to control what label text is rendered for a given <see cref="Increment"/>
/// when <see cref="ShowLabelsEvery"/> is above 0
/// </summary>
public LabelGetterDelegate LabelGetter;
/// <summary>
/// Displayed below/to left of labels (see <see cref="Orientation"/>).
/// If text is not visible, check <see cref="GraphView.MarginBottom"/> / <see cref="GraphView.MarginLeft"/>
/// </summary>
public string Text;
/// <summary>
/// The minimum axis point to show. Defaults to null (no minimum)
/// </summary>
public float? Minimum { get; set; }
/// <summary>
/// Populates base properties and sets the read only <see cref="Orientation"/>
/// </summary>
/// <param name="orientation"></param>
protected Axis (Orientation orientation)
{
Orientation = orientation;
LabelGetter = DefaultLabelGetter;
}
/// <summary>
/// Draws the solid line of the axis
/// </summary>
/// <param name="graph"></param>
public abstract void DrawAxisLine (GraphView graph);
/// <summary>
/// Draws a single cell of the solid line of the axis
/// </summary>
/// <param name="graph"></param>
/// <param name="x"></param>
/// <param name="y"></param>
protected abstract void DrawAxisLine (GraphView graph, int x, int y);
/// <summary>
/// Draws labels and axis <see cref="Increment"/> ticks
/// </summary>
/// <param name="graph"></param>
public abstract void DrawAxisLabels (GraphView graph);
/// <summary>
/// Draws a custom label <paramref name="text"/> at <paramref name="screenPosition"/> units
/// along the axis (X or Y depending on <see cref="Orientation"/>)
/// </summary>
/// <param name="graph"></param>
/// <param name="screenPosition"></param>
/// <param name="text"></param>
public abstract void DrawAxisLabel (GraphView graph, int screenPosition, string text);
/// <summary>
/// Resets all configurable properties of the axis to default values
/// </summary>
public virtual void Reset ()
{
Increment = 1;
ShowLabelsEvery = DefaultShowLabelsEvery;
Visible = true;
Text = "";
LabelGetter = DefaultLabelGetter;
Minimum = null;
}
private string DefaultLabelGetter (AxisIncrementToRender toRender)
{
return toRender.Value.ToString ("N0");
}
}
/// <summary>
/// The horizontal (x axis) of a <see cref="GraphView"/>
/// </summary>
public class HorizontalAxis : Axis {
/// <summary>
/// Creates a new instance of axis with an <see cref="Orientation"/> of <see cref="Orientation.Horizontal"/>
/// </summary>
public HorizontalAxis () : base (Orientation.Horizontal)
{
}
/// <summary>
/// Draws the horizontal axis line
/// </summary>
/// <param name="graph"></param>
public override void DrawAxisLine (GraphView graph)
{
if (!Visible) {
return;
}
var bounds = graph.Bounds;
graph.Move (0, 0);
var y = GetAxisYPosition (graph);
// start the x axis at left of screen (either 0 or margin)
var xStart = (int)graph.MarginLeft;
// but if the x axis has a minmum (minimum is in graph space units)
if (Minimum.HasValue) {
// start at the screen location of the minimum
var minimumScreenX = graph.GraphSpaceToScreen (new PointF (Minimum.Value, y)).X;
// unless that is off the screen to the left
xStart = Math.Max (xStart, minimumScreenX);
}
for (int i = xStart; i < bounds.Width; i++) {
DrawAxisLine (graph, i, y);
}
}
/// <summary>
/// Draws a horizontal axis line at the given <paramref name="x"/>, <paramref name="y"/>
/// screen coordinates
/// </summary>
/// <param name="graph"></param>
/// <param name="x"></param>
/// <param name="y"></param>
protected override void DrawAxisLine (GraphView graph, int x, int y)
{
graph.Move (x, y);
Application.Driver.AddRune (Application.Driver.HLine);
}
/// <summary>
/// Draws the horizontal x axis labels and <see cref="Axis.Increment"/> ticks
/// </summary>
public override void DrawAxisLabels (GraphView graph)
{
if (!Visible || Increment == 0) {
return;
}
var bounds = graph.Bounds;
var labels = GetLabels (graph, bounds);
foreach (var label in labels) {
DrawAxisLabel (graph, label.ScreenLocation, label.Text);
}
// if there is a title
if (!string.IsNullOrWhiteSpace (Text)) {
string toRender = Text;
// if label is too long
if (toRender.Length > graph.Bounds.Width) {
toRender = toRender.Substring (0, graph.Bounds.Width);
}
graph.Move (graph.Bounds.Width / 2 - (toRender.Length / 2), graph.Bounds.Height - 1);
Application.Driver.AddStr (toRender);
}
}
/// <summary>
/// Draws the given <paramref name="text"/> on the axis at x <paramref name="screenPosition"/>.
/// For the screen y position use <see cref="GetAxisYPosition(GraphView)"/>
/// </summary>
/// <param name="graph">Graph being drawn onto</param>
/// <param name="screenPosition">Number of screen columns along the axis to take before rendering</param>
/// <param name="text">Text to render under the axis tick</param>
public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
{
var driver = Application.Driver;
var y = GetAxisYPosition (graph);
graph.Move (screenPosition, y);
// draw the tick on the axis
driver.AddRune (driver.TopTee);
// and the label text
if (!string.IsNullOrWhiteSpace (text)) {
// center the label but don't draw it outside bounds of the graph
int drawAtX = Math.Max (0, screenPosition - (text.Length / 2));
string toRender = text;
// this is how much space is left
int xSpaceAvailable = graph.Bounds.Width - drawAtX;
// There is no space for the label at all!
if (xSpaceAvailable <= 0) {
return;
}
// if we are close to right side of graph, don't overspill
if (toRender.Length > xSpaceAvailable) {
toRender = toRender.Substring (0, xSpaceAvailable);
}
graph.Move (drawAtX, Math.Min (y + 1, graph.Bounds.Height - 1));
driver.AddStr (toRender);
}
}
private IEnumerable<AxisIncrementToRender> GetLabels (GraphView graph, Rect bounds)
{
// if no labels
if (Increment == 0) {
yield break;
}
int labels = 0;
int y = GetAxisYPosition (graph);
var start = graph.ScreenToGraphSpace (0, y);
var end = graph.ScreenToGraphSpace (bounds.Width, y);
// don't draw labels below the minimum
if (Minimum.HasValue) {
start.X = Math.Max (start.X, Minimum.Value);
}
var current = start;
while (current.X < end.X) {
int screenX = graph.GraphSpaceToScreen (new PointF (current.X, current.Y)).X;
// Ensure the axis point does not draw into the margin
if (screenX >= graph.MarginLeft) {
// The increment we will render (normally a top T unicode symbol)
var toRender = new AxisIncrementToRender (Orientation, screenX, current.X);
// Not every increment has to have a label
if (ShowLabelsEvery != 0) {
// if this increment does also needs a label
if (labels++ % ShowLabelsEvery == 0) {
toRender.Text = LabelGetter (toRender);
};
}
// Label or no label definetly render it
yield return toRender;
}
current.X += Increment;
}
}
/// <summary>
/// Returns the Y screen position of the origin (typically 0,0) of graph space.
/// Return value is bounded by the screen i.e. the axis is always rendered even
/// if the origin is offscreen.
/// </summary>
/// <param name="graph"></param>
public int GetAxisYPosition (GraphView graph)
{
// find the origin of the graph in screen space (this allows for 'crosshair' style
// graphs where positive and negative numbers visible
var origin = graph.GraphSpaceToScreen (new PointF (0, 0));
// float the X axis so that it accurately represents the origin of the graph
// but anchor it to top/bottom if the origin is offscreen
return Math.Min (Math.Max (0, origin.Y), graph.Bounds.Height - ((int)graph.MarginBottom + 1));
}
}
/// <summary>
/// The vertical (i.e. Y axis) of a <see cref="GraphView"/>
/// </summary>
public class VerticalAxis : Axis {
/// <summary>
/// Creates a new <see cref="Orientation.Vertical"/> axis
/// </summary>
public VerticalAxis () : base (Orientation.Vertical)
{
}
/// <summary>
/// Draws the vertical axis line
/// </summary>
/// <param name="graph"></param>
public override void DrawAxisLine (GraphView graph)
{
if (!Visible) {
return;
}
Rect bounds = graph.Bounds;
var x = GetAxisXPosition (graph);
var yEnd = GetAxisYEnd (graph);
// don't draw down further than the control bounds
yEnd = Math.Min (yEnd, bounds.Height - (int)graph.MarginBottom);
// Draw solid line
for (int i = 0; i < yEnd; i++) {
DrawAxisLine (graph, x, i);
}
}
/// <summary>
/// Draws a vertical axis line at the given <paramref name="x"/>, <paramref name="y"/>
/// screen coordinates
/// </summary>
/// <param name="graph"></param>
/// <param name="x"></param>
/// <param name="y"></param>
protected override void DrawAxisLine (GraphView graph, int x, int y)
{
graph.Move (x, y);
Application.Driver.AddRune (Application.Driver.VLine);
}
private int GetAxisYEnd (GraphView graph)
{
// draw down the screen (0 is top of screen)
// end at the bottom of the screen
//unless there is a minimum
if (Minimum.HasValue) {
return graph.GraphSpaceToScreen (new PointF (0, Minimum.Value)).Y;
}
return graph.Bounds.Height;
}
/// <summary>
/// Draws axis <see cref="Axis.Increment"/> markers and labels
/// </summary>
/// <param name="graph"></param>
public override void DrawAxisLabels (GraphView graph)
{
if (!Visible || Increment == 0) {
return;
}
var bounds = graph.Bounds;
var labels = GetLabels (graph, bounds);
foreach (var label in labels) {
DrawAxisLabel (graph, label.ScreenLocation, label.Text);
}
// if there is a title
if (!string.IsNullOrWhiteSpace (Text)) {
string toRender = Text;
// if label is too long
if (toRender.Length > graph.Bounds.Height) {
toRender = toRender.Substring (0, graph.Bounds.Height);
}
// Draw it 1 letter at a time vertically down row 0 of the control
int startDrawingAtY = graph.Bounds.Height / 2 - (toRender.Length / 2);
for (int i = 0; i < toRender.Length; i++) {
graph.Move (0, startDrawingAtY + i);
Application.Driver.AddRune (toRender [i]);
}
}
}
private IEnumerable<AxisIncrementToRender> GetLabels (GraphView graph, Rect bounds)
{
// if no labels
if (Increment == 0) {
yield break;
}
int labels = 0;
int x = GetAxisXPosition (graph);
// remember screen space is top down so the lowest graph
// space value is at the bottom of the screen
var start = graph.ScreenToGraphSpace (x, bounds.Height - 1);
var end = graph.ScreenToGraphSpace (x, 0);
// don't draw labels below the minimum
if (Minimum.HasValue) {
start.Y = Math.Max (start.Y, Minimum.Value);
}
var current = start;
var dontDrawBelowScreenY = bounds.Height - graph.MarginBottom;
while (current.Y < end.Y) {
int screenY = graph.GraphSpaceToScreen (new PointF (current.X, current.Y)).Y;
// if the axis label is above the bottom margin (screen y starts at 0 at the top)
if (screenY < dontDrawBelowScreenY) {
// Create the axis symbol
var toRender = new AxisIncrementToRender (Orientation, screenY, current.Y);
// and the label (if we are due one)
if (ShowLabelsEvery != 0) {
// if this increment also needs a label
if (labels++ % ShowLabelsEvery == 0) {
toRender.Text = LabelGetter (toRender);
};
}
// draw the axis symbol (and label if it has one)
yield return toRender;
}
current.Y += Increment;
}
}
/// <summary>
/// Draws the given <paramref name="text"/> on the axis at y <paramref name="screenPosition"/>.
/// For the screen x position use <see cref="GetAxisXPosition(GraphView)"/>
/// </summary>
/// <param name="graph">Graph being drawn onto</param>
/// <param name="screenPosition">Number of rows from the top of the screen (i.e. down the axis) before rendering</param>
/// <param name="text">Text to render to the left of the axis tick. Ensure to
/// set <see cref="GraphView.MarginLeft"/> or <see cref="GraphView.ScrollOffset"/> sufficient that it is visible</param>
public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
{
var x = GetAxisXPosition (graph);
var labelThickness = text.Length;
graph.Move (x, screenPosition);
// draw the tick on the axis
Application.Driver.AddRune (Application.Driver.RightTee);
// and the label text
if (!string.IsNullOrWhiteSpace (text)) {
graph.Move (Math.Max (0, x - labelThickness), screenPosition);
Application.Driver.AddStr (text);
}
}
/// <summary>
/// Returns the X screen position of the origin (typically 0,0) of graph space.
/// Return value is bounded by the screen i.e. the axis is always rendered even
/// if the origin is offscreen.
/// </summary>
/// <param name="graph"></param>
public int GetAxisXPosition (GraphView graph)
{
// find the origin of the graph in screen space (this allows for 'crosshair' style
// graphs where positive and negative numbers visible
var origin = graph.GraphSpaceToScreen (new PointF (0, 0));
// float the Y axis so that it accurately represents the origin of the graph
// but anchor it to left/right if the origin is offscreen
return Math.Min (Math.Max ((int)graph.MarginLeft, origin.X), graph.Bounds.Width - 1);
}
}
/// <summary>
/// A location on an axis of a <see cref="GraphView"/> that may
/// or may not have a label associated with it
/// </summary>
public class AxisIncrementToRender {
/// <summary>
/// Direction of the parent axis
/// </summary>
public Orientation Orientation { get; }
/// <summary>
/// The screen location (X or Y depending on <see cref="Orientation"/>) that the
/// increment will be rendered at
/// </summary>
public int ScreenLocation { get; }
/// <summary>
/// The value at this position on the axis in graph space
/// </summary>
public float Value { get; }
private string _text = "";
/// <summary>
/// The text (if any) that should be displayed at this axis increment
/// </summary>
/// <value></value>
internal string Text {
get => _text;
set { _text = value ?? ""; }
}
/// <summary>
/// Describe a new section of an axis that requires an axis increment
/// symbol and/or label
/// </summary>
/// <param name="orientation"></param>
/// <param name="screen"></param>
/// <param name="value"></param>
public AxisIncrementToRender (Orientation orientation, int screen, float value)
{
Orientation = orientation;
ScreenLocation = screen;
Value = value;
}
}
/// <summary>
/// Delegate for custom formatting of axis labels. Determines what should be displayed at a given label
/// </summary>
/// <param name="toRender">The axis increment to which the label is attached</param>
/// <returns></returns>
public delegate string LabelGetterDelegate (AxisIncrementToRender toRender);
}

View File

@@ -0,0 +1,45 @@
using System;
namespace Terminal.Gui.Graphs {
/// <summary>
/// Describes how to render a single row/column of a <see cref="GraphView"/> based
/// on the value(s) in <see cref="ISeries"/> at that location
/// </summary>
public class GraphCellToRender {
/// <summary>
/// The character to render in the console
/// </summary>
public Rune Rune { get; set; }
/// <summary>
/// Optional color to render the <see cref="Rune"/> with
/// </summary>
public Attribute? Color { get; set; }
/// <summary>
/// Creates instance and sets <see cref="Rune"/> with default graph coloring
/// </summary>
/// <param name="rune"></param>
public GraphCellToRender (Rune rune)
{
Rune = rune;
}
/// <summary>
/// Creates instance and sets <see cref="Rune"/> with custom graph coloring
/// </summary>
/// <param name="rune"></param>
/// <param name="color"></param>
public GraphCellToRender (Rune rune, Attribute color) : this (rune)
{
Color = color;
}
/// <summary>
/// Creates instance and sets <see cref="Rune"/> and <see cref="Color"/> (or default if null)
/// </summary>
public GraphCellToRender (Rune rune, Attribute? color) : this (rune)
{
Color = color;
}
}
}

View File

@@ -0,0 +1,17 @@
namespace Terminal.Gui.Graphs {
/// <summary>
/// Direction of an element (horizontal or vertical)
/// </summary>
public enum Orientation {
/// <summary>
/// Left to right
/// </summary>
Horizontal,
/// <summary>
/// Bottom to top
/// </summary>
Vertical
}
}

View File

@@ -0,0 +1,323 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Terminal.Gui.Graphs {
/// <summary>
/// Describes a series of data that can be rendered into a <see cref="GraphView"/>>
/// </summary>
public interface ISeries {
/// <summary>
/// Draws the <paramref name="graphBounds"/> section of a series into the
/// <paramref name="graph"/> view <paramref name="drawBounds"/>
/// </summary>
/// <param name="graph">Graph series is to be drawn onto</param>
/// <param name="drawBounds">Visible area of the graph in Console Screen units (excluding margins)</param>
/// <param name="graphBounds">Visible area of the graph in Graph space units</param>
void DrawSeries (GraphView graph, Rect drawBounds, RectangleF graphBounds);
}
/// <summary>
/// Series composed of any number of discrete data points
/// </summary>
public class ScatterSeries : ISeries {
/// <summary>
/// Collection of each discrete point in the series
/// </summary>
/// <returns></returns>
public List<PointF> Points { get; set; } = new List<PointF> ();
/// <summary>
/// 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'
/// </summary>
public GraphCellToRender Fill { get; set; } = new GraphCellToRender ('x');
/// <summary>
/// Draws all points directly onto the graph
/// </summary>
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);
}
}
}
/// <summary>
/// Collection of <see cref="BarSeries"/> in which bars are clustered by category
/// </summary>
public class MultiBarSeries : ISeries {
BarSeries [] subSeries;
/// <summary>
/// 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
/// </summary>
public IReadOnlyCollection<BarSeries> SubSeries { get => new ReadOnlyCollection<BarSeries> (subSeries); }
/// <summary>
/// The number of units of graph space between bars. Should be
/// less than <see cref="BarSeries.BarEvery"/>
/// </summary>
public float Spacing { get; }
/// <summary>
/// Creates a new series of clustered bars.
/// </summary>
/// <param name="numberOfBarsPerCategory">Each category has this many bars</param>
/// <param name="barsEvery">How far appart to put each category (in graph space)</param>
/// <param name="spacing">How much spacing between bars in a category (should be less than <paramref name="barsEvery"/>/<paramref name="numberOfBarsPerCategory"/>)</param>
/// <param name="colors">Array of colors that define bar color in each category. Length must match <paramref name="numberOfBarsPerCategory"/></param>
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;
}
/// <summary>
/// Adds a new cluster of bars
/// </summary>
/// <param name="label"></param>
/// <param name="fill"></param>
/// <param name="values">Values for each bar in category, must match the number of bars per category</param>
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]));
}
}
/// <summary>
/// Draws all <see cref="SubSeries"/>
/// </summary>
/// <param name="graph"></param>
/// <param name="drawBounds"></param>
/// <param name="graphBounds"></param>
public void DrawSeries (GraphView graph, Rect drawBounds, RectangleF graphBounds)
{
foreach (var bar in subSeries) {
bar.DrawSeries (graph, drawBounds, graphBounds);
}
}
}
/// <summary>
/// Series of bars positioned at regular intervals
/// </summary>
public class BarSeries : ISeries {
/// <summary>
/// Ordered collection of graph bars to position along axis
/// </summary>
public List<Bar> Bars { get; set; } = new List<Bar> ();
/// <summary>
/// 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 <see cref="GraphView.CellSize"/> when changing this.
/// </summary>
public float BarEvery { get; set; } = 1;
/// <summary>
/// Direction bars protrude from the corresponding axis.
/// Defaults to vertical
/// </summary>
public Orientation Orientation { get; set; } = Orientation.Vertical;
/// <summary>
/// The number of units of graph space along the axis before rendering the first bar
/// (and subsequent bars - see <see cref="BarEvery"/>). Defaults to 0
/// </summary>
public float Offset { get; set; } = 0;
/// <summary>
/// Overrides the <see cref="Bar.Fill"/> with a fixed color
/// </summary>
public Attribute? OverrideBarColor { get; set; }
/// <summary>
/// True to draw <see cref="Bar.Text"/> along the axis under the bar. Defaults
/// to true.
/// </summary>
public bool DrawLabels { get; set; } = true;
/// <summary>
/// Applies any color overriding
/// </summary>
/// <param name="graphCellToRender"></param>
/// <returns></returns>
protected virtual GraphCellToRender AdjustColor (GraphCellToRender graphCellToRender)
{
if (OverrideBarColor.HasValue) {
graphCellToRender.Color = OverrideBarColor;
}
return graphCellToRender;
}
/// <summary>
/// Draws bars that are currently in the <paramref name="drawBounds"/>
/// </summary>
/// <param name="graph"></param>
/// <param name="drawBounds">Screen area of the graph excluding margins</param>
/// <param name="graphBounds">Graph space area that should be drawn into <paramref name="drawBounds"/></param>
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);
}
}
}
}
/// <summary>
/// Override to do custom drawing of the bar e.g. to apply varying color or changing the fill
/// symbol mid bar.
/// </summary>
/// <param name="graph"></param>
/// <param name="start">Screen position of the start of the bar</param>
/// <param name="end">Screen position of the end of the bar</param>
/// <param name="beingDrawn">The Bar that occupies this space and is being drawn</param>
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 ();
}
/// <summary>
/// A single bar in a <see cref="BarSeries"/>
/// </summary>
public class Bar {
/// <summary>
/// Optional text that describes the bar. This will be rendered on the corresponding
/// <see cref="Axis"/> unless <see cref="DrawLabels"/> is false
/// </summary>
public string Text { get; set; }
/// <summary>
/// The color and character that will be rendered in the console
/// when the bar extends over it
/// </summary>
public GraphCellToRender Fill { get; set; }
/// <summary>
/// The value in graph space X/Y (depending on <see cref="Orientation"/>) to which the bar extends.
/// </summary>
public float Value { get; }
/// <summary>
/// Creates a new instance of a single bar rendered in the given <paramref name="fill"/> that extends
/// out <paramref name="value"/> graph space units in the default <see cref="Orientation"/>
/// </summary>
/// <param name="text"></param>
/// <param name="fill"></param>
/// <param name="value"></param>
public Bar (string text, GraphCellToRender fill, float value)
{
Text = text;
Fill = fill;
Value = value;
}
}
}
}

View File

@@ -0,0 +1,138 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Copied from: https://github.com/dotnet/corefx/tree/master/src/System.Drawing.Primitives/src/System/Drawing
using System;
using System.ComponentModel;
namespace Terminal.Gui {
/// <summary>
/// Represents an ordered pair of x and y coordinates that define a point in a two-dimensional plane.
/// </summary>
public struct PointF : IEquatable<PointF> {
/// <summary>
/// Creates a new instance of the <see cref='Terminal.Gui.PointF'/> class with member data left uninitialized.
/// </summary>
public static readonly PointF Empty;
private float x; // Do not rename (binary serialization)
private float y; // Do not rename (binary serialization)
/// <summary>
/// Initializes a new instance of the <see cref='Terminal.Gui.PointF'/> class with the specified coordinates.
/// </summary>
public PointF (float x, float y)
{
this.x = x;
this.y = y;
}
/// <summary>
/// Gets a value indicating whether this <see cref='Terminal.Gui.PointF'/> is empty.
/// </summary>
[Browsable (false)]
public bool IsEmpty => x == 0f && y == 0f;
/// <summary>
/// Gets the x-coordinate of this <see cref='Terminal.Gui.PointF'/>.
/// </summary>
public float X {
get => x;
set => x = value;
}
/// <summary>
/// Gets the y-coordinate of this <see cref='Terminal.Gui.PointF'/>.
/// </summary>
public float Y {
get => y;
set => y = value;
}
/// <summary>
/// Translates a <see cref='Terminal.Gui.PointF'/> by a given <see cref='Terminal.Gui.Size'/> .
/// </summary>
public static PointF operator + (PointF pt, Size sz) => Add (pt, sz);
/// <summary>
/// Translates a <see cref='Terminal.Gui.PointF'/> by the negative of a given <see cref='Terminal.Gui.Size'/> .
/// </summary>
public static PointF operator - (PointF pt, Size sz) => Subtract (pt, sz);
/// <summary>
/// Translates a <see cref='Terminal.Gui.PointF'/> by a given <see cref='Terminal.Gui.SizeF'/> .
/// </summary>
public static PointF operator + (PointF pt, SizeF sz) => Add (pt, sz);
/// <summary>
/// Translates a <see cref='Terminal.Gui.PointF'/> by the negative of a given <see cref='Terminal.Gui.SizeF'/> .
/// </summary>
public static PointF operator - (PointF pt, SizeF sz) => Subtract (pt, sz);
/// <summary>
/// Compares two <see cref='Terminal.Gui.PointF'/> objects. The result specifies whether the values of the
/// <see cref='Terminal.Gui.PointF.X'/> and <see cref='Terminal.Gui.PointF.Y'/> properties of the two
/// <see cref='Terminal.Gui.PointF'/> objects are equal.
/// </summary>
public static bool operator == (PointF left, PointF right) => left.X == right.X && left.Y == right.Y;
/// <summary>
/// Compares two <see cref='Terminal.Gui.PointF'/> objects. The result specifies whether the values of the
/// <see cref='Terminal.Gui.PointF.X'/> or <see cref='Terminal.Gui.PointF.Y'/> properties of the two
/// <see cref='Terminal.Gui.PointF'/> objects are unequal.
/// </summary>
public static bool operator != (PointF left, PointF right) => !(left == right);
/// <summary>
/// Translates a <see cref='Terminal.Gui.PointF'/> by a given <see cref='Terminal.Gui.Size'/> .
/// </summary>
public static PointF Add (PointF pt, Size sz) => new PointF (pt.X + sz.Width, pt.Y + sz.Height);
/// <summary>
/// Translates a <see cref='Terminal.Gui.PointF'/> by the negative of a given <see cref='Terminal.Gui.Size'/> .
/// </summary>
public static PointF Subtract (PointF pt, Size sz) => new PointF (pt.X - sz.Width, pt.Y - sz.Height);
/// <summary>
/// Translates a <see cref='Terminal.Gui.PointF'/> by a given <see cref='Terminal.Gui.SizeF'/> .
/// </summary>
public static PointF Add (PointF pt, SizeF sz) => new PointF (pt.X + sz.Width, pt.Y + sz.Height);
/// <summary>
/// Translates a <see cref='Terminal.Gui.PointF'/> by the negative of a given <see cref='Terminal.Gui.SizeF'/> .
/// </summary>
public static PointF Subtract (PointF pt, SizeF sz) => new PointF (pt.X - sz.Width, pt.Y - sz.Height);
/// <summary>
/// Compares two <see cref='Terminal.Gui.PointF'/> objects. The result specifies whether the values of the
/// <see cref='Terminal.Gui.PointF.X'/> and <see cref='Terminal.Gui.PointF.Y'/> properties of the two
/// <see cref='Terminal.Gui.PointF'/> objects are equal.
/// </summary>
public override bool Equals (object obj) => obj is PointF && Equals ((PointF)obj);
/// <summary>
/// Compares two <see cref='Terminal.Gui.PointF'/> objects. The result specifies whether the values of the
/// <see cref='Terminal.Gui.PointF.X'/> and <see cref='Terminal.Gui.PointF.Y'/> properties of the two
/// <see cref='Terminal.Gui.PointF'/> objects are equal.
/// </summary>
public bool Equals (PointF other) => this == other;
/// <summary>
/// Generates a hashcode from the X and Y components
/// </summary>
/// <returns></returns>
public override int GetHashCode ()
{
return X.GetHashCode() ^ Y.GetHashCode ();
}
/// <summary>
/// Returns a string including the X and Y values
/// </summary>
/// <returns></returns>
public override string ToString () => "{X=" + x.ToString () + ", Y=" + y.ToString () + "}";
}
}

View File

@@ -0,0 +1,303 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Copied from https://github.com/dotnet/corefx/tree/master/src/System.Drawing.Primitives/src/System/Drawing
using System;
using System.ComponentModel;
namespace Terminal.Gui {
/// <summary>
/// Stores the location and size of a rectangular region.
/// </summary>
public struct RectangleF : IEquatable<RectangleF> {
/// <summary>
/// Initializes a new instance of the <see cref='Terminal.Gui.RectangleF'/> class.
/// </summary>
public static readonly RectangleF Empty;
private float x; // Do not rename (binary serialization)
private float y; // Do not rename (binary serialization)
private float width; // Do not rename (binary serialization)
private float height; // Do not rename (binary serialization)
/// <summary>
/// Initializes a new instance of the <see cref='Terminal.Gui.RectangleF'/> class with the specified location
/// and size.
/// </summary>
public RectangleF (float x, float y, float width, float height)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
/// <summary>
/// Initializes a new instance of the <see cref='Terminal.Gui.RectangleF'/> class with the specified location
/// and size.
/// </summary>
public RectangleF (PointF location, SizeF size)
{
x = location.X;
y = location.Y;
width = size.Width;
height = size.Height;
}
/// <summary>
/// Creates a new <see cref='Terminal.Gui.RectangleF'/> with the specified location and size.
/// </summary>
public static RectangleF FromLTRB (float left, float top, float right, float bottom) =>
new RectangleF (left, top, right - left, bottom - top);
/// <summary>
/// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this
/// <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
[Browsable (false)]
public PointF Location {
get => new PointF (X, Y);
set {
X = value.X;
Y = value.Y;
}
}
/// <summary>
/// Gets or sets the size of this <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
[Browsable (false)]
public SizeF Size {
get => new SizeF (Width, Height);
set {
Width = value.Width;
Height = value.Height;
}
}
/// <summary>
/// Gets or sets the x-coordinate of the upper-left corner of the rectangular region defined by this
/// <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
public float X {
get => x;
set => x = value;
}
/// <summary>
/// Gets or sets the y-coordinate of the upper-left corner of the rectangular region defined by this
/// <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
public float Y {
get => y;
set => y = value;
}
/// <summary>
/// Gets or sets the width of the rectangular region defined by this <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
public float Width {
get => width;
set => width = value;
}
/// <summary>
/// Gets or sets the height of the rectangular region defined by this <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
public float Height {
get => height;
set => height = value;
}
/// <summary>
/// Gets the x-coordinate of the upper-left corner of the rectangular region defined by this
/// <see cref='Terminal.Gui.RectangleF'/> .
/// </summary>
[Browsable (false)]
public float Left => X;
/// <summary>
/// Gets the y-coordinate of the upper-left corner of the rectangular region defined by this
/// <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
[Browsable (false)]
public float Top => Y;
/// <summary>
/// Gets the x-coordinate of the lower-right corner of the rectangular region defined by this
/// <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
[Browsable (false)]
public float Right => X + Width;
/// <summary>
/// Gets the y-coordinate of the lower-right corner of the rectangular region defined by this
/// <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
[Browsable (false)]
public float Bottom => Y + Height;
/// <summary>
/// Tests whether this <see cref='Terminal.Gui.RectangleF'/> has a <see cref='Terminal.Gui.RectangleF.Width'/> or a <see cref='Terminal.Gui.RectangleF.Height'/> of 0.
/// </summary>
[Browsable (false)]
public bool IsEmpty => (Width <= 0) || (Height <= 0);
/// <summary>
/// Tests whether <paramref name="obj"/> is a <see cref='Terminal.Gui.RectangleF'/> with the same location and
/// size of this <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
public override bool Equals (object obj) => obj is RectangleF && Equals ((RectangleF)obj);
/// <summary>
/// Returns true if two <see cref='Terminal.Gui.RectangleF'/> objects have equal location and size.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool Equals (RectangleF other) => this == other;
/// <summary>
/// Tests whether two <see cref='Terminal.Gui.RectangleF'/> objects have equal location and size.
/// </summary>
public static bool operator == (RectangleF left, RectangleF right) =>
left.X == right.X && left.Y == right.Y && left.Width == right.Width && left.Height == right.Height;
/// <summary>
/// Tests whether two <see cref='Terminal.Gui.RectangleF'/> objects differ in location or size.
/// </summary>
public static bool operator != (RectangleF left, RectangleF right) => !(left == right);
/// <summary>
/// Determines if the specified point is contained within the rectangular region defined by this
/// <see cref='Terminal.Gui.Rect'/> .
/// </summary>
public bool Contains (float x, float y) => X <= x && x < X + Width && Y <= y && y < Y + Height;
/// <summary>
/// Determines if the specified point is contained within the rectangular region defined by this
/// <see cref='Terminal.Gui.Rect'/> .
/// </summary>
public bool Contains (PointF pt) => Contains (pt.X, pt.Y);
/// <summary>
/// Determines if the rectangular region represented by <paramref name="rect"/> is entirely contained within
/// the rectangular region represented by this <see cref='Terminal.Gui.Rect'/> .
/// </summary>
public bool Contains (RectangleF rect) =>
(X <= rect.X) && (rect.X + rect.Width <= X + Width) && (Y <= rect.Y) && (rect.Y + rect.Height <= Y + Height);
/// <summary>
/// Gets the hash code for this <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
public override int GetHashCode ()
{
return (Height.GetHashCode () + Width.GetHashCode ()) ^ X.GetHashCode () + Y.GetHashCode ();
}
/// <summary>
/// Inflates this <see cref='Terminal.Gui.Rect'/> by the specified amount.
/// </summary>
public void Inflate (float x, float y)
{
X -= x;
Y -= y;
Width += 2 * x;
Height += 2 * y;
}
/// <summary>
/// Inflates this <see cref='Terminal.Gui.Rect'/> by the specified amount.
/// </summary>
public void Inflate (SizeF size) => Inflate (size.Width, size.Height);
/// <summary>
/// Creates a <see cref='Terminal.Gui.Rect'/> that is inflated by the specified amount.
/// </summary>
public static RectangleF Inflate (RectangleF rect, float x, float y)
{
RectangleF r = rect;
r.Inflate (x, y);
return r;
}
/// <summary>
/// Creates a Rectangle that represents the intersection between this Rectangle and rect.
/// </summary>
public void Intersect (RectangleF rect)
{
RectangleF result = Intersect (rect, this);
X = result.X;
Y = result.Y;
Width = result.Width;
Height = result.Height;
}
/// <summary>
/// Creates a rectangle that represents the intersection between a and b. If there is no intersection, an
/// empty rectangle is returned.
/// </summary>
public static RectangleF Intersect (RectangleF a, RectangleF b)
{
float x1 = Math.Max (a.X, b.X);
float x2 = Math.Min (a.X + a.Width, b.X + b.Width);
float y1 = Math.Max (a.Y, b.Y);
float y2 = Math.Min (a.Y + a.Height, b.Y + b.Height);
if (x2 >= x1 && y2 >= y1) {
return new RectangleF (x1, y1, x2 - x1, y2 - y1);
}
return Empty;
}
/// <summary>
/// Determines if this rectangle intersects with rect.
/// </summary>
public bool IntersectsWith (RectangleF rect) =>
(rect.X < X + Width) && (X < rect.X + rect.Width) && (rect.Y < Y + Height) && (Y < rect.Y + rect.Height);
/// <summary>
/// Creates a rectangle that represents the union between a and b.
/// </summary>
public static RectangleF Union (RectangleF a, RectangleF b)
{
float x1 = Math.Min (a.X, b.X);
float x2 = Math.Max (a.X + a.Width, b.X + b.Width);
float y1 = Math.Min (a.Y, b.Y);
float y2 = Math.Max (a.Y + a.Height, b.Y + b.Height);
return new RectangleF (x1, y1, x2 - x1, y2 - y1);
}
/// <summary>
/// Adjusts the location of this rectangle by the specified amount.
/// </summary>
public void Offset (PointF pos) => Offset (pos.X, pos.Y);
/// <summary>
/// Adjusts the location of this rectangle by the specified amount.
/// </summary>
public void Offset (float x, float y)
{
X += x;
Y += y;
}
/// <summary>
/// Converts the specified <see cref='Terminal.Gui.Rect'/> to a
/// <see cref='Terminal.Gui.RectangleF'/>.
/// </summary>
public static implicit operator RectangleF (Rect r) => new RectangleF (r.X, r.Y, r.Width, r.Height);
/// <summary>
/// Converts the <see cref='Terminal.Gui.RectangleF.Location'/> and <see cref='Terminal.Gui.RectangleF.Size'/>
/// of this <see cref='Terminal.Gui.RectangleF'/> to a human-readable string.
/// </summary>
public override string ToString () =>
"{X=" + X.ToString () + ",Y=" + Y.ToString () +
",Width=" + Width.ToString () + ",Height=" + Height.ToString () + "}";
}
}

168
Terminal.Gui/Types/SizeF.cs Normal file
View File

@@ -0,0 +1,168 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Copied from: https://github.com/dotnet/corefx/tree/master/src/System.Drawing.Primitives/src/System/Drawing
using System;
using System.ComponentModel;
namespace Terminal.Gui {
/// <summary>
/// Represents the size of a rectangular region with an ordered pair of width and height.
/// </summary>
public struct SizeF : IEquatable<SizeF> {
/// <summary>
/// Initializes a new instance of the <see cref='Terminal.Gui.SizeF'/> class.
/// </summary>
public static readonly SizeF Empty;
private float width; // Do not rename (binary serialization)
private float height; // Do not rename (binary serialization)
/// <summary>
/// Initializes a new instance of the <see cref='Terminal.Gui.SizeF'/> class from the specified
/// existing <see cref='Terminal.Gui.SizeF'/>.
/// </summary>
public SizeF (SizeF size)
{
width = size.width;
height = size.height;
}
/// <summary>
/// Initializes a new instance of the <see cref='Terminal.Gui.SizeF'/> class from the specified
/// <see cref='Terminal.Gui.PointF'/>.
/// </summary>
public SizeF (PointF pt)
{
width = pt.X;
height = pt.Y;
}
/// <summary>
/// Initializes a new instance of the <see cref='Terminal.Gui.SizeF'/> class from the specified dimensions.
/// </summary>
public SizeF (float width, float height)
{
this.width = width;
this.height = height;
}
/// <summary>
/// Performs vector addition of two <see cref='Terminal.Gui.SizeF'/> objects.
/// </summary>
public static SizeF operator + (SizeF sz1, SizeF sz2) => Add (sz1, sz2);
/// <summary>
/// Contracts a <see cref='Terminal.Gui.SizeF'/> by another <see cref='Terminal.Gui.SizeF'/>
/// </summary>
public static SizeF operator - (SizeF sz1, SizeF sz2) => Subtract (sz1, sz2);
/// <summary>
/// Multiplies <see cref="SizeF"/> by a <see cref="float"/> producing <see cref="SizeF"/>.
/// </summary>
/// <param name="left">Multiplier of type <see cref="float"/>.</param>
/// <param name="right">Multiplicand of type <see cref="SizeF"/>.</param>
/// <returns>Product of type <see cref="SizeF"/>.</returns>
public static SizeF operator * (float left, SizeF right) => Multiply (right, left);
/// <summary>
/// Multiplies <see cref="SizeF"/> by a <see cref="float"/> producing <see cref="SizeF"/>.
/// </summary>
/// <param name="left">Multiplicand of type <see cref="SizeF"/>.</param>
/// <param name="right">Multiplier of type <see cref="float"/>.</param>
/// <returns>Product of type <see cref="SizeF"/>.</returns>
public static SizeF operator * (SizeF left, float right) => Multiply (left, right);
/// <summary>
/// Divides <see cref="SizeF"/> by a <see cref="float"/> producing <see cref="SizeF"/>.
/// </summary>
/// <param name="left">Dividend of type <see cref="SizeF"/>.</param>
/// <param name="right">Divisor of type <see cref="int"/>.</param>
/// <returns>Result of type <see cref="SizeF"/>.</returns>
public static SizeF operator / (SizeF left, float right)
=> new SizeF (left.width / right, left.height / right);
/// <summary>
/// Tests whether two <see cref='Terminal.Gui.SizeF'/> objects are identical.
/// </summary>
public static bool operator == (SizeF sz1, SizeF sz2) => sz1.Width == sz2.Width && sz1.Height == sz2.Height;
/// <summary>
/// Tests whether two <see cref='Terminal.Gui.SizeF'/> objects are different.
/// </summary>
public static bool operator != (SizeF sz1, SizeF sz2) => !(sz1 == sz2);
/// <summary>
/// Converts the specified <see cref='Terminal.Gui.SizeF'/> to a <see cref='Terminal.Gui.PointF'/>.
/// </summary>
public static explicit operator PointF (SizeF size) => new PointF (size.Width, size.Height);
/// <summary>
/// Tests whether this <see cref='Terminal.Gui.SizeF'/> has zero width and height.
/// </summary>
[Browsable (false)]
public bool IsEmpty => width == 0 && height == 0;
/// <summary>
/// Represents the horizontal component of this <see cref='Terminal.Gui.SizeF'/>.
/// </summary>
public float Width {
get => width;
set => width = value;
}
/// <summary>
/// Represents the vertical component of this <see cref='Terminal.Gui.SizeF'/>.
/// </summary>
public float Height {
get => height;
set => height = value;
}
/// <summary>
/// Performs vector addition of two <see cref='Terminal.Gui.SizeF'/> objects.
/// </summary>
public static SizeF Add (SizeF sz1, SizeF sz2) => new SizeF (sz1.Width + sz2.Width, sz1.Height + sz2.Height);
/// <summary>
/// Contracts a <see cref='Terminal.Gui.SizeF'/> by another <see cref='Terminal.Gui.SizeF'/>.
/// </summary>
public static SizeF Subtract (SizeF sz1, SizeF sz2) => new SizeF (sz1.Width - sz2.Width, sz1.Height - sz2.Height);
/// <summary>
/// Tests to see whether the specified object is a <see cref='Terminal.Gui.SizeF'/> with the same dimensions
/// as this <see cref='Terminal.Gui.SizeF'/>.
/// </summary>
public override bool Equals (object obj) => obj is SizeF && Equals ((SizeF)obj);
/// <summary>
/// Tests whether two <see cref='Terminal.Gui.SizeF'/> objects are identical.
/// </summary>
public bool Equals (SizeF other) => this == other;
/// <summary>
/// Generates a hashcode from the width and height
/// </summary>
/// <returns></returns>
public override int GetHashCode ()
{
return width.GetHashCode() ^ height.GetHashCode ();
}
/// <summary>
/// Creates a human-readable string that represents this <see cref='Terminal.Gui.SizeF'/>.
/// </summary>
public override string ToString () => "{Width=" + width.ToString () + ", Height=" + height.ToString () + "}";
/// <summary>
/// Multiplies <see cref="SizeF"/> by a <see cref="float"/> producing <see cref="SizeF"/>.
/// </summary>
/// <param name="size">Multiplicand of type <see cref="SizeF"/>.</param>
/// <param name="multiplier">Multiplier of type <see cref="float"/>.</param>
/// <returns>Product of type SizeF.</returns>
private static SizeF Multiply (SizeF size, float multiplier) =>
new SizeF (size.width * multiplier, size.height * multiplier);
}
}

View File

@@ -0,0 +1,318 @@
using NStack;
using System;
using System.Collections.Generic;
using System.Linq;
using Terminal.Gui.Graphs;
namespace Terminal.Gui {
/// <summary>
/// Control for rendering graphs (bar, scatter etc)
/// </summary>
public class GraphView : View {
/// <summary>
/// Horizontal axis
/// </summary>
/// <value></value>
public HorizontalAxis AxisX { get; set; }
/// <summary>
/// Vertical axis
/// </summary>
/// <value></value>
public VerticalAxis AxisY { get; set; }
/// <summary>
/// Collection of data series that are rendered in the graph
/// </summary>
public List<ISeries> Series { get; } = new List<ISeries> ();
/// <summary>
/// Elements drawn into graph after series have been drawn e.g. Legends etc
/// </summary>
public List<IAnnotation> Annotations { get; } = new List<IAnnotation> ();
/// <summary>
/// Amount of space to leave on left of control. Graph content (<see cref="Series"/>)
/// will not be rendered in margins but axis labels may be
/// </summary>
public uint MarginLeft { get; set; }
/// <summary>
/// Amount of space to leave on bottom of control. Graph content (<see cref="Series"/>)
/// will not be rendered in margins but axis labels may be
/// </summary>
public uint MarginBottom { get; set; }
/// <summary>
/// The graph space position of the bottom left of the control.
/// Changing this scrolls the viewport around in the graph
/// </summary>
/// <value></value>
public PointF ScrollOffset { get; set; } = new PointF (0, 0);
/// <summary>
/// Translates console width/height into graph space. Defaults
/// to 1 row/col of console space being 1 unit of graph space.
/// </summary>
/// <returns></returns>
public PointF CellSize { get; set; } = new PointF (1, 1);
/// <summary>
/// The color of the background of the graph and axis/labels
/// </summary>
public Attribute? GraphColor { get; set; }
/// <summary>
/// Creates a new graph with a 1 to 1 graph space with absolute layout
/// </summary>
public GraphView ()
{
CanFocus = true;
AxisX = new HorizontalAxis ();
AxisY = new VerticalAxis ();
}
/// <summary>
/// Clears all settings configured on the graph and resets all properties
/// to default values (<see cref="CellSize"/>, <see cref="ScrollOffset"/> etc)
/// </summary>
public void Reset ()
{
ScrollOffset = new PointF (0, 0);
CellSize = new PointF (1, 1);
AxisX.Reset ();
AxisY.Reset ();
Series.Clear ();
Annotations.Clear ();
GraphColor = null;
SetNeedsDisplay ();
}
///<inheritdoc/>
public override void Redraw (Rect bounds)
{
if(CellSize.X == 0 || CellSize.Y == 0) {
throw new Exception ($"{nameof(CellSize)} cannot be 0");
}
SetDriverColorToGraphColor ();
Move (0, 0);
// clear all old content
for (int i = 0; i < Bounds.Height; i++) {
Move (0, i);
Driver.AddStr (new string (' ', Bounds.Width));
}
// If there is no data do not display a graph
if (!Series.Any () && !Annotations.Any ()) {
return;
}
// Draw 'before' annotations
foreach (var a in Annotations.Where (a => a.BeforeSeries)) {
a.Render (this);
}
SetDriverColorToGraphColor ();
AxisY.DrawAxisLine (this);
AxisX.DrawAxisLine (this);
AxisY.DrawAxisLabels (this);
AxisX.DrawAxisLabels (this);
// Draw a cross where the two axis cross
var axisIntersection = new Point(AxisY.GetAxisXPosition(this),AxisX.GetAxisYPosition(this));
if (AxisX.Visible && AxisY.Visible) {
Move (axisIntersection.X, axisIntersection.Y);
AddRune (axisIntersection.X, axisIntersection.Y, '\u253C');
}
SetDriverColorToGraphColor ();
// The drawable area of the graph (anything that isn't in the margins)
Rect drawBounds = new Rect((int)MarginLeft,0, Bounds.Width - ((int)MarginLeft), Bounds.Height - (int)MarginBottom);
RectangleF graphSpace = ScreenToGraphSpace (drawBounds);
foreach (var s in Series) {
s.DrawSeries (this, drawBounds, graphSpace);
// If a series changes the graph color reset it
SetDriverColorToGraphColor ();
}
SetDriverColorToGraphColor ();
// Draw 'after' annotations
foreach (var a in Annotations.Where (a => !a.BeforeSeries)) {
a.Render (this);
}
}
/// <summary>
/// Sets the color attribute of <see cref="Application.Driver"/> to the <see cref="GraphColor"/>
/// (if defined) or <see cref="ColorScheme"/> otherwise.
/// </summary>
public void SetDriverColorToGraphColor ()
{
Driver.SetAttribute (GraphColor ?? ColorScheme.Normal);
}
/// <summary>
/// Returns the section of the graph that is represented by the given
/// screen position
/// </summary>
/// <param name="col"></param>
/// <param name="row"></param>
/// <returns></returns>
public RectangleF ScreenToGraphSpace (int col, int row)
{
return new RectangleF (
ScrollOffset.X + ((col - MarginLeft) * CellSize.X),
ScrollOffset.Y + ((Bounds.Height - (row + MarginBottom + 1)) * CellSize.Y),
CellSize.X, CellSize.Y);
}
/// <summary>
/// Returns the section of the graph that is represented by the screen area
/// </summary>
/// <param name="screenArea"></param>
/// <returns></returns>
public RectangleF ScreenToGraphSpace (Rect screenArea)
{
// get position of the bottom left
var pos = ScreenToGraphSpace (screenArea.Left, screenArea.Bottom-1);
return new RectangleF (pos.X, pos.Y, screenArea.Width * CellSize.X, screenArea.Height * CellSize.Y);
}
/// <summary>
/// Calculates the screen location for a given point in graph space.
/// Bear in mind these be off screen
/// </summary>
/// <param name="location">Point in graph space that may or may not be represented in the
/// visible area of graph currently presented. E.g. 0,0 for origin</param>
/// <returns>Screen position (Column/Row) which would be used to render the graph <paramref name="location"/>.
/// Note that this can be outside the current client area of the control</returns>
public Point GraphSpaceToScreen (PointF location)
{
return new Point (
(int)((location.X - ScrollOffset.X) / CellSize.X) + (int)MarginLeft,
// screen coordinates are top down while graph coordinates are bottom up
(Bounds.Height - 1) - (int)MarginBottom - (int)((location.Y - ScrollOffset.Y) / CellSize.Y)
);
}
/// <inheritdoc/>
public override bool ProcessKey (KeyEvent keyEvent)
{
//&& Focused == tabsBar
if (HasFocus && CanFocus) {
switch (keyEvent.Key) {
case Key.CursorLeft:
Scroll (-CellSize.X, 0);
return true;
case Key.CursorLeft | Key.CtrlMask:
Scroll (-CellSize.X * 5, 0);
return true;
case Key.CursorRight:
Scroll (CellSize.X, 0);
return true;
case Key.CursorRight | Key.CtrlMask:
Scroll (CellSize.X * 5, 0);
return true;
case Key.CursorDown:
Scroll (0, -CellSize.Y);
return true;
case Key.CursorDown | Key.CtrlMask:
Scroll (0, -CellSize.Y * 5);
return true;
case Key.CursorUp:
Scroll (0, CellSize.Y);
return true;
case Key.CursorUp | Key.CtrlMask:
Scroll (0, CellSize.Y * 5);
return true;
}
}
return base.ProcessKey (keyEvent);
}
/// <summary>
/// Scrolls the view by a given number of units in graph space.
/// See <see cref="CellSize"/> to translate this into rows/cols
/// </summary>
/// <param name="offsetX"></param>
/// <param name="offsetY"></param>
private void Scroll (float offsetX, float offsetY)
{
ScrollOffset = new PointF (
ScrollOffset.X + offsetX,
ScrollOffset.Y + offsetY);
SetNeedsDisplay ();
}
#region Bresenham's line algorithm
// https://rosettacode.org/wiki/Bitmap/Bresenham%27s_line_algorithm#C.23
int ipart (decimal x) { return (int)x; }
decimal fpart (decimal x)
{
if (x < 0) return (1 - (x - Math.Floor (x)));
return (x - Math.Floor (x));
}
/// <summary>
/// Draws a line between two points in screen space. Can be diagonals.
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="symbol">The symbol to use for the line</param>
public void DrawLine (Point start, Point end, Rune symbol)
{
if (Equals (start, end)) {
return;
}
int x0 = start.X;
int y0 = start.Y;
int x1 = end.X;
int y1 = end.Y;
int dx = Math.Abs (x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = Math.Abs (y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = (dx > dy ? dx : -dy) / 2, e2;
while (true) {
AddRune (x0, y0, symbol);
if (x0 == x1 && y0 == y1) break;
e2 = err;
if (e2 > -dx) { err -= dy; x0 += sx; }
if (e2 < dy) { err += dx; y0 += sy; }
}
}
#endregion
}
}

View File

@@ -0,0 +1,685 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Terminal.Gui;
using Terminal.Gui.Graphs;
using Color = Terminal.Gui.Color;
namespace UICatalog.Scenarios {
[ScenarioMetadata (Name: "Graph View", Description: "Demos GraphView control")]
[ScenarioCategory ("Controls")]
class GraphViewExample : Scenario {
GraphView graphView;
private TextView about;
int currentGraph = 0;
Action [] graphs;
public override void Setup ()
{
Win.Title = this.GetName ();
Win.Y = 1; // menu
Win.Height = Dim.Fill (1); // status bar
Top.LayoutSubviews ();
graphs = new Action [] {
()=>SetupPeriodicTableScatterPlot(), //0
()=>SetupLifeExpectancyBarGraph(true), //1
()=>SetupLifeExpectancyBarGraph(false), //2
()=>SetupPopulationPyramid(), //3
()=>SetupLineGraph(), //4
()=>SetupSineWave(), //5
()=>SetupDisco(), //6
()=>MultiBarGraph() //7
};
var menu = new MenuBar (new MenuBarItem [] {
new MenuBarItem ("_File", new MenuItem [] {
new MenuItem ("Scatter _Plot", "",()=>graphs[currentGraph = 0]()),
new MenuItem ("_V Bar Graph", "", ()=>graphs[currentGraph = 1]()),
new MenuItem ("_H Bar Graph", "", ()=>graphs[currentGraph = 2]()) ,
new MenuItem ("P_opulation Pyramid","",()=>graphs[currentGraph = 3]()),
new MenuItem ("_Line Graph","",()=>graphs[currentGraph = 4]()),
new MenuItem ("Sine _Wave","",()=>graphs[currentGraph = 5]()),
new MenuItem ("Silent _Disco","",()=>graphs[currentGraph = 6]()),
new MenuItem ("_Multi Bar Graph","",()=>graphs[currentGraph = 7]()),
new MenuItem ("_Quit", "", () => Quit()),
}),
new MenuBarItem ("_View", new MenuItem [] {
new MenuItem ("Zoom _In", "", () => Zoom(0.5f)),
new MenuItem ("Zoom _Out", "", () => Zoom(2f)),
}),
});
Top.Add (menu);
graphView = new GraphView () {
X = 1,
Y = 1,
Width = 60,
Height = 20,
};
Win.Add (graphView);
var frameRight = new FrameView ("About") {
X = Pos.Right (graphView) + 1,
Y = 0,
Width = Dim.Fill (),
Height = Dim.Fill (),
};
frameRight.Add (about = new TextView () {
Width = Dim.Fill (),
Height = Dim.Fill ()
});
Win.Add (frameRight);
var statusBar = new StatusBar (new StatusItem [] {
new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
new StatusItem(Key.CtrlMask | Key.G, "~^G~ Next", ()=>graphs[currentGraph++%graphs.Length]()),
});
Top.Add (statusBar);
}
private void MultiBarGraph ()
{
graphView.Reset ();
about.Text = "Housing Expenditures by income thirds 1996-2003";
var black = Application.Driver.MakeAttribute (graphView.ColorScheme.Normal.Foreground, Color.Black);
var cyan = Application.Driver.MakeAttribute (Color.BrightCyan, Color.Black);
var magenta = Application.Driver.MakeAttribute (Color.BrightMagenta, Color.Black);
var red = Application.Driver.MakeAttribute (Color.BrightRed, Color.Black);
graphView.GraphColor = black;
var series = new MultiBarSeries (3, 1, 0.25f, new [] { magenta, cyan, red });
var stiple = Application.Driver.Stipple;
series.AddBars ("'96", stiple, 5900, 9000, 14000);
series.AddBars ("'97", stiple, 6100, 9200, 14800);
series.AddBars ("'98", stiple, 6000, 9300, 14600);
series.AddBars ("'99", stiple, 6100, 9400, 14950);
series.AddBars ("'00", stiple, 6200, 9500, 15200);
series.AddBars ("'01", stiple, 6250, 9900, 16000);
series.AddBars ("'02", stiple, 6600, 11000, 16700);
series.AddBars ("'03", stiple, 7000, 12000, 17000);
graphView.CellSize = new PointF (0.25f, 1000);
graphView.Series.Add (series);
graphView.SetNeedsDisplay ();
graphView.MarginLeft = 3;
graphView.MarginBottom = 1;
graphView.AxisY.LabelGetter = (v) => '$' + (v.Value / 1000f).ToString ("N0") + 'k';
// Do not show x axis labels (bars draw their own labels)
graphView.AxisX.Increment = 0;
graphView.AxisX.ShowLabelsEvery = 0;
graphView.AxisX.Minimum = 0;
graphView.AxisY.Minimum = 0;
var legend = new LegendAnnotation (new Rect (graphView.Bounds.Width - 20,0, 20, 5));
legend.AddEntry (new GraphCellToRender (stiple, series.SubSeries.ElementAt (0).OverrideBarColor), "Lower Third");
legend.AddEntry (new GraphCellToRender (stiple, series.SubSeries.ElementAt (1).OverrideBarColor), "Middle Third");
legend.AddEntry (new GraphCellToRender (stiple, series.SubSeries.ElementAt (2).OverrideBarColor), "Upper Third");
graphView.Annotations.Add (legend);
}
private void SetupLineGraph ()
{
graphView.Reset ();
about.Text = "This graph shows random points";
var black = Application.Driver.MakeAttribute (graphView.ColorScheme.Normal.Foreground, Color.Black);
var cyan = Application.Driver.MakeAttribute (Color.BrightCyan, Color.Black);
var magenta = Application.Driver.MakeAttribute (Color.BrightMagenta, Color.Black);
var red = Application.Driver.MakeAttribute (Color.BrightRed, Color.Black);
graphView.GraphColor = black;
List<PointF> randomPoints = new List<PointF> ();
Random r = new Random ();
for (int i = 0; i < 10; i++) {
randomPoints.Add (new PointF (r.Next (100), r.Next (100)));
}
var points = new ScatterSeries () {
Points = randomPoints
};
var line = new PathAnnotation () {
LineColor = cyan,
Points = randomPoints.OrderBy (p => p.X).ToList (),
BeforeSeries = true,
};
graphView.Series.Add (points);
graphView.Annotations.Add (line);
randomPoints = new List<PointF> ();
for (int i = 0; i < 10; i++) {
randomPoints.Add (new PointF (r.Next (100), r.Next (100)));
}
var points2 = new ScatterSeries () {
Points = randomPoints,
Fill = new GraphCellToRender ('x', red)
};
var line2 = new PathAnnotation () {
LineColor = magenta,
Points = randomPoints.OrderBy (p => p.X).ToList (),
BeforeSeries = true,
};
graphView.Series.Add (points2);
graphView.Annotations.Add (line2);
// How much graph space each cell of the console depicts
graphView.CellSize = new PointF (2, 5);
// leave space for axis labels
graphView.MarginBottom = 2;
graphView.MarginLeft = 3;
// One axis tick/label per
graphView.AxisX.Increment = 20;
graphView.AxisX.ShowLabelsEvery = 1;
graphView.AxisX.Text = "X →";
graphView.AxisY.Increment = 20;
graphView.AxisY.ShowLabelsEvery = 1;
graphView.AxisY.Text = "↑Y";
var max = line.Points.Union (line2.Points).OrderByDescending (p => p.Y).First ();
graphView.Annotations.Add (new TextAnnotation () { Text = "(Max)", GraphPosition = new PointF (max.X + (2 * graphView.CellSize.X), max.Y) });
graphView.SetNeedsDisplay ();
}
private void SetupSineWave ()
{
graphView.Reset ();
about.Text = "This graph shows a sine wave";
var points = new ScatterSeries ();
var line = new PathAnnotation ();
// Draw line first so it does not draw over top of points or axis labels
line.BeforeSeries = true;
// Generate line graph with 2,000 points
for (float x = -500; x < 500; x += 0.5f) {
points.Points.Add (new PointF (x, (float)Math.Sin (x)));
line.Points.Add (new PointF (x, (float)Math.Sin (x)));
}
graphView.Series.Add (points);
graphView.Annotations.Add (line);
// How much graph space each cell of the console depicts
graphView.CellSize = new PointF (0.1f, 0.1f);
// leave space for axis labels
graphView.MarginBottom = 2;
graphView.MarginLeft = 3;
// One axis tick/label per
graphView.AxisX.Increment = 0.5f;
graphView.AxisX.ShowLabelsEvery = 2;
graphView.AxisX.Text = "X →";
graphView.AxisX.LabelGetter = (v) => v.Value.ToString ("N2");
graphView.AxisY.Increment = 0.2f;
graphView.AxisY.ShowLabelsEvery = 2;
graphView.AxisY.Text = "↑Y";
graphView.AxisY.LabelGetter = (v) => v.Value.ToString ("N2");
graphView.ScrollOffset = new PointF (-2.5f, -1);
graphView.SetNeedsDisplay ();
}
/*
Country,Both,Male,Female
"Switzerland",83.4,81.8,85.1
"South Korea",83.3,80.3,86.1
"Singapore",83.2,81,85.5
"Spain",83.2,80.7,85.7
"Cyprus",83.1,81.1,85.1
"Australia",83,81.3,84.8
"Italy",83,80.9,84.9
"Norway",83,81.2,84.7
"Israel",82.6,80.8,84.4
"France",82.5,79.8,85.1
"Luxembourg",82.4,80.6,84.2
"Sweden",82.4,80.8,84
"Iceland",82.3,80.8,83.9
"Canada",82.2,80.4,84.1
"New Zealand",82,80.4,83.5
"Malta,81.9",79.9,83.8
"Ireland",81.8,80.2,83.5
"Netherlands",81.8,80.4,83.1
"Germany",81.7,78.7,84.8
"Austria",81.6,79.4,83.8
"Finland",81.6,79.2,84
"Portugal",81.6,78.6,84.4
"Belgium",81.4,79.3,83.5
"United Kingdom",81.4,79.8,83
"Denmark",81.3,79.6,83
"Slovenia",81.3,78.6,84.1
"Greece",81.1,78.6,83.6
"Kuwait",81,79.3,83.9
"Costa Rica",80.8,78.3,83.4*/
private void SetupLifeExpectancyBarGraph (bool verticalBars)
{
graphView.Reset ();
about.Text = "This graph shows the life expectancy at birth of a range of countries";
var softStiple = new GraphCellToRender ('\u2591');
var mediumStiple = new GraphCellToRender ('\u2592');
var barSeries = new BarSeries () {
Bars = new List<BarSeries.Bar> () {
new BarSeries.Bar ("Switzerland", softStiple, 83.4f),
new BarSeries.Bar ("South Korea", !verticalBars?mediumStiple:softStiple, 83.3f),
new BarSeries.Bar ("Singapore", softStiple, 83.2f),
new BarSeries.Bar ("Spain", !verticalBars?mediumStiple:softStiple, 83.2f),
new BarSeries.Bar ("Cyprus", softStiple, 83.1f),
new BarSeries.Bar ("Australia", !verticalBars?mediumStiple:softStiple, 83),
new BarSeries.Bar ("Italy", softStiple, 83),
new BarSeries.Bar ("Norway", !verticalBars?mediumStiple:softStiple, 83),
new BarSeries.Bar ("Israel", softStiple, 82.6f),
new BarSeries.Bar ("France", !verticalBars?mediumStiple:softStiple, 82.5f),
new BarSeries.Bar ("Luxembourg", softStiple, 82.4f),
new BarSeries.Bar ("Sweden", !verticalBars?mediumStiple:softStiple, 82.4f),
new BarSeries.Bar ("Iceland", softStiple, 82.3f),
new BarSeries.Bar ("Canada", !verticalBars?mediumStiple:softStiple, 82.2f),
new BarSeries.Bar ("New Zealand", softStiple, 82),
new BarSeries.Bar ("Malta", !verticalBars?mediumStiple:softStiple, 81.9f),
new BarSeries.Bar ("Ireland", softStiple, 81.8f)
}
};
graphView.Series.Add (barSeries);
if (verticalBars) {
barSeries.Orientation = Orientation.Vertical;
// How much graph space each cell of the console depicts
graphView.CellSize = new PointF (0.1f, 0.25f);
// No axis marks since Bar will add it's own categorical marks
graphView.AxisX.Increment = 0f;
graphView.AxisX.Text = "Country";
graphView.AxisX.Minimum = 0;
graphView.AxisY.Increment = 1f;
graphView.AxisY.ShowLabelsEvery = 1;
graphView.AxisY.LabelGetter = v => v.Value.ToString ("N2");
graphView.AxisY.Minimum = 0;
graphView.AxisY.Text = "Age";
// leave space for axis labels and title
graphView.MarginBottom = 2;
graphView.MarginLeft = 6;
// Start the graph at 80 years because that is where most of our data is
graphView.ScrollOffset = new PointF (0, 80);
} else {
barSeries.Orientation = Orientation.Horizontal;
// How much graph space each cell of the console depicts
graphView.CellSize = new PointF (0.1f, 1f);
// No axis marks since Bar will add it's own categorical marks
graphView.AxisY.Increment = 0f;
graphView.AxisY.ShowLabelsEvery = 1;
graphView.AxisY.Text = "Country";
graphView.AxisY.Minimum = 0;
graphView.AxisX.Increment = 1f;
graphView.AxisX.ShowLabelsEvery = 1;
graphView.AxisX.LabelGetter = v => v.Value.ToString ("N2");
graphView.AxisX.Text = "Age";
graphView.AxisX.Minimum = 0;
// leave space for axis labels and title
graphView.MarginBottom = 2;
graphView.MarginLeft = (uint)barSeries.Bars.Max (b => b.Text.Length) + 2;
// Start the graph at 80 years because that is where most of our data is
graphView.ScrollOffset = new PointF (80, 0);
}
graphView.SetNeedsDisplay ();
}
private void SetupPopulationPyramid ()
{
/*
Age,M,F
0-4,2009363,1915127
5-9,2108550,2011016
10-14,2022370,1933970
15-19,1880611,1805522
20-24,2072674,2001966
25-29,2275138,2208929
30-34,2361054,2345774
35-39,2279836,2308360
40-44,2148253,2159877
45-49,2128343,2167778
50-54,2281421,2353119
55-59,2232388,2306537
60-64,1919839,1985177
65-69,1647391,1734370
70-74,1624635,1763853
75-79,1137438,1304709
80-84,766956,969611
85-89,438663,638892
90-94,169952,320625
95-99,34524,95559
100+,3016,12818*/
about.Text = "This graph shows population of each age divided by gender";
graphView.Reset ();
// How much graph space each cell of the console depicts
graphView.CellSize = new PointF (100_000, 1);
//center the x axis in middle of screen to show both sides
graphView.ScrollOffset = new PointF (-3_000_000, 0);
graphView.AxisX.Text = "Number Of People";
graphView.AxisX.Increment = 500_000;
graphView.AxisX.ShowLabelsEvery = 2;
// use Abs to make negative axis labels positive
graphView.AxisX.LabelGetter = (v) => Math.Abs (v.Value / 1_000_000).ToString ("N2") + "M";
// leave space for axis labels
graphView.MarginBottom = 2;
graphView.MarginLeft = 1;
// do not show axis titles (bars have their own categories)
graphView.AxisY.Increment = 0;
graphView.AxisY.ShowLabelsEvery = 0;
graphView.AxisY.Minimum = 0;
var stiple = new GraphCellToRender (Application.Driver.Stipple);
// Bars in 2 directions
// Males (negative to make the bars go left)
var malesSeries = new BarSeries () {
Orientation = Orientation.Horizontal,
Bars = new List<BarSeries.Bar> ()
{
new BarSeries.Bar("0-4",stiple,-2009363),
new BarSeries.Bar("5-9",stiple,-2108550),
new BarSeries.Bar("10-14",stiple,-2022370),
new BarSeries.Bar("15-19",stiple,-1880611),
new BarSeries.Bar("20-24",stiple,-2072674),
new BarSeries.Bar("25-29",stiple,-2275138),
new BarSeries.Bar("30-34",stiple,-2361054),
new BarSeries.Bar("35-39",stiple,-2279836),
new BarSeries.Bar("40-44",stiple,-2148253),
new BarSeries.Bar("45-49",stiple,-2128343),
new BarSeries.Bar("50-54",stiple,-2281421),
new BarSeries.Bar("55-59",stiple,-2232388),
new BarSeries.Bar("60-64",stiple,-1919839),
new BarSeries.Bar("65-69",stiple,-1647391),
new BarSeries.Bar("70-74",stiple,-1624635),
new BarSeries.Bar("75-79",stiple,-1137438),
new BarSeries.Bar("80-84",stiple,-766956),
new BarSeries.Bar("85-89",stiple,-438663),
new BarSeries.Bar("90-94",stiple,-169952),
new BarSeries.Bar("95-99",stiple,-34524),
new BarSeries.Bar("100+",stiple,-3016)
}
};
graphView.Series.Add (malesSeries);
// Females
var femalesSeries = new BarSeries () {
Orientation = Orientation.Horizontal,
Bars = new List<BarSeries.Bar> ()
{
new BarSeries.Bar("0-4",stiple,1915127),
new BarSeries.Bar("5-9",stiple,2011016),
new BarSeries.Bar("10-14",stiple,1933970),
new BarSeries.Bar("15-19",stiple,1805522),
new BarSeries.Bar("20-24",stiple,2001966),
new BarSeries.Bar("25-29",stiple,2208929),
new BarSeries.Bar("30-34",stiple,2345774),
new BarSeries.Bar("35-39",stiple,2308360),
new BarSeries.Bar("40-44",stiple,2159877),
new BarSeries.Bar("45-49",stiple,2167778),
new BarSeries.Bar("50-54",stiple,2353119),
new BarSeries.Bar("55-59",stiple,2306537),
new BarSeries.Bar("60-64",stiple,1985177),
new BarSeries.Bar("65-69",stiple,1734370),
new BarSeries.Bar("70-74",stiple,1763853),
new BarSeries.Bar("75-79",stiple,1304709),
new BarSeries.Bar("80-84",stiple,969611),
new BarSeries.Bar("85-89",stiple,638892),
new BarSeries.Bar("90-94",stiple,320625),
new BarSeries.Bar("95-99",stiple,95559),
new BarSeries.Bar("100+",stiple,12818)
}
};
var softStiple = new GraphCellToRender ('\u2591');
var mediumStiple = new GraphCellToRender ('\u2592');
for (int i = 0; i < malesSeries.Bars.Count; i++) {
malesSeries.Bars [i].Fill = i % 2 == 0 ? softStiple : mediumStiple;
femalesSeries.Bars [i].Fill = i % 2 == 0 ? softStiple : mediumStiple;
}
graphView.Series.Add (femalesSeries);
graphView.Annotations.Add (new TextAnnotation () { Text = "M", ScreenPosition = new Terminal.Gui.Point (0, 10) });
graphView.Annotations.Add (new TextAnnotation () { Text = "F", ScreenPosition = new Terminal.Gui.Point (graphView.Bounds.Width - 1, 10) });
graphView.SetNeedsDisplay ();
}
class DiscoBarSeries : BarSeries {
private Terminal.Gui.Attribute green;
private Terminal.Gui.Attribute brightgreen;
private Terminal.Gui.Attribute brightyellow;
private Terminal.Gui.Attribute red;
private Terminal.Gui.Attribute brightred;
public DiscoBarSeries ()
{
green = Application.Driver.MakeAttribute (Color.BrightGreen, Color.Black);
brightgreen = Application.Driver.MakeAttribute (Color.Green, Color.Black);
brightyellow = Application.Driver.MakeAttribute (Color.BrightYellow, Color.Black);
red = Application.Driver.MakeAttribute (Color.Red, Color.Black);
brightred = Application.Driver.MakeAttribute (Color.BrightRed, Color.Black);
}
protected override void DrawBarLine (GraphView graph, Terminal.Gui.Point start, Terminal.Gui.Point end, Bar beingDrawn)
{
var driver = Application.Driver;
int x = start.X;
for(int y = end.Y; y <= start.Y; y++) {
var height = graph.ScreenToGraphSpace (x, y).Y;
if (height >= 85) {
driver.SetAttribute(red);
}
else
if (height >= 66) {
driver.SetAttribute (brightred);
}
else
if (height >= 45) {
driver.SetAttribute (brightyellow);
}
else
if (height >= 25) {
driver.SetAttribute (brightgreen);
}
else{
driver.SetAttribute (green);
}
graph.AddRune (x, y, beingDrawn.Fill.Rune);
}
}
}
private void SetupDisco ()
{
graphView.Reset ();
about.Text = "This graph shows a graphic equaliser for an imaginary song";
graphView.GraphColor = Application.Driver.MakeAttribute (Color.White, Color.Black);
var stiple = new GraphCellToRender ('\u2593');
Random r = new Random ();
var series = new DiscoBarSeries ();
var bars = new List<BarSeries.Bar> ();
Func<MainLoop, bool> genSample = (l) => {
bars.Clear ();
// generate an imaginary sample
for (int i = 0; i < 31; i++) {
bars.Add (
new BarSeries.Bar (null, stiple, r.Next (0, 100)) {
//ColorGetter = colorDelegate
});
}
graphView.SetNeedsDisplay ();
// while the equaliser is showing
return graphView.Series.Contains (series);
};
Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (250), genSample);
series.Bars = bars;
graphView.Series.Add (series);
// How much graph space each cell of the console depicts
graphView.CellSize = new PointF (1, 10);
graphView.AxisX.Increment = 0; // No graph ticks
graphView.AxisX.ShowLabelsEvery = 0; // no labels
graphView.AxisX.Visible = false;
graphView.AxisY.Visible = false;
graphView.SetNeedsDisplay ();
}
private void SetupPeriodicTableScatterPlot ()
{
graphView.Reset ();
about.Text = "This graph shows the atomic weight of each element in the periodic table.\nStarting with Hydrogen (atomic Number 1 with a weight of 1.007)";
//AtomicNumber and AtomicMass of all elements in the periodic table
graphView.Series.Add (
new ScatterSeries () {
Points = new List<PointF>{
new PointF(1,1.007f),new PointF(2,4.002f),new PointF(3,6.941f),new PointF(4,9.012f),new PointF(5,10.811f),new PointF(6,12.011f),
new PointF(7,14.007f),new PointF(8,15.999f),new PointF(9,18.998f),new PointF(10,20.18f),new PointF(11,22.99f),new PointF(12,24.305f),
new PointF(13,26.982f),new PointF(14,28.086f),new PointF(15,30.974f),new PointF(16,32.065f),new PointF(17,35.453f),new PointF(18,39.948f),
new PointF(19,39.098f),new PointF(20,40.078f),new PointF(21,44.956f),new PointF(22,47.867f),new PointF(23,50.942f),new PointF(24,51.996f),
new PointF(25,54.938f),new PointF(26,55.845f),new PointF(27,58.933f),new PointF(28,58.693f),new PointF(29,63.546f),new PointF(30,65.38f),
new PointF(31,69.723f),new PointF(32,72.64f),new PointF(33,74.922f),new PointF(34,78.96f),new PointF(35,79.904f),new PointF(36,83.798f),
new PointF(37,85.468f),new PointF(38,87.62f),new PointF(39,88.906f),new PointF(40,91.224f),new PointF(41,92.906f),new PointF(42,95.96f),
new PointF(43,98f),new PointF(44,101.07f),new PointF(45,102.906f),new PointF(46,106.42f),new PointF(47,107.868f),new PointF(48,112.411f),
new PointF(49,114.818f),new PointF(50,118.71f),new PointF(51,121.76f),new PointF(52,127.6f),new PointF(53,126.904f),new PointF(54,131.293f),
new PointF(55,132.905f),new PointF(56,137.327f),new PointF(57,138.905f),new PointF(58,140.116f),new PointF(59,140.908f),new PointF(60,144.242f),
new PointF(61,145),new PointF(62,150.36f),new PointF(63,151.964f),new PointF(64,157.25f),new PointF(65,158.925f),new PointF(66,162.5f),
new PointF(67,164.93f),new PointF(68,167.259f),new PointF(69,168.934f),new PointF(70,173.054f),new PointF(71,174.967f),new PointF(72,178.49f),
new PointF(73,180.948f),new PointF(74,183.84f),new PointF(75,186.207f),new PointF(76,190.23f),new PointF(77,192.217f),new PointF(78,195.084f),
new PointF(79,196.967f),new PointF(80,200.59f),new PointF(81,204.383f),new PointF(82,207.2f),new PointF(83,208.98f),new PointF(84,210),
new PointF(85,210),new PointF(86,222),new PointF(87,223),new PointF(88,226),new PointF(89,227),new PointF(90,232.038f),new PointF(91,231.036f),
new PointF(92,238.029f),new PointF(93,237),new PointF(94,244),new PointF(95,243),new PointF(96,247),new PointF(97,247),new PointF(98,251),
new PointF(99,252),new PointF(100,257),new PointF(101,258),new PointF(102,259),new PointF(103,262),new PointF(104,261),new PointF(105,262),
new PointF(106,266),new PointF(107,264),new PointF(108,267),new PointF(109,268),new PointF(113,284),new PointF(114,289),new PointF(115,288),
new PointF(116,292),new PointF(117,295),new PointF(118,294)
}
});
// How much graph space each cell of the console depicts
graphView.CellSize = new PointF (1, 5);
// leave space for axis labels
graphView.MarginBottom = 2;
graphView.MarginLeft = 3;
// One axis tick/label per 5 atomic numbers
graphView.AxisX.Increment = 5;
graphView.AxisX.ShowLabelsEvery = 1;
graphView.AxisX.Text = "Atomic Number";
graphView.AxisX.Minimum = 0;
// One label every 5 atomic weight
graphView.AxisY.Increment = 5;
graphView.AxisY.ShowLabelsEvery = 1;
graphView.AxisY.Minimum = 0;
graphView.SetNeedsDisplay ();
}
private void Zoom (float factor)
{
graphView.CellSize = new PointF (
graphView.CellSize.X * factor,
graphView.CellSize.Y * factor
);
graphView.AxisX.Increment *= factor;
graphView.AxisY.Increment *= factor;
graphView.SetNeedsDisplay ();
}
private void Quit ()
{
Application.RequestStop ();
}
}
}

1307
UnitTests/GraphViewTests.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ using Xunit;
using System.Globalization;
namespace Terminal.Gui.Views {
public class TabViewTests {
private TabView GetTabView ()
{

View File

@@ -8,6 +8,7 @@ using Xunit;
using System.Globalization;
namespace Terminal.Gui.Views {
public class TableViewTests
{

View File

@@ -8,6 +8,7 @@ using Terminal.Gui.Trees;
using Xunit;
namespace Terminal.Gui.Views {
public class TreeViewTests {
#region Test Setup Methods
class Factory {