namespace Terminal.Gui;
/// Renders a continuous line with grid line ticks and labels
public abstract class Axis
{
/// Default value for
private const uint DefaultShowLabelsEvery = 5;
///
/// Allows you to control what label text is rendered for a given when
/// is above 0
///
public LabelGetterDelegate LabelGetter;
/// Populates base properties and sets the read only
///
protected Axis (Orientation orientation)
{
Orientation = orientation;
LabelGetter = DefaultLabelGetter;
}
/// Number of units of graph space between ticks on axis. 0 for no ticks
///
public float Increment { get; set; } = 1;
/// The minimum axis point to show. Defaults to null (no minimum)
public float? Minimum { get; set; }
/// Direction of the axis
///
public Orientation Orientation { get; }
/// The number of before an label is added. 0 = never show labels
public uint ShowLabelsEvery { get; set; } = DefaultShowLabelsEvery;
///
/// Displayed below/to left of labels (see ). If text is not visible, check
/// /
///
public string Text { get; set; }
/// True to render axis. Defaults to true
public bool Visible { get; set; } = true;
///
/// Draws a custom label at units along the axis (X or Y
/// depending on )
///
///
///
///
public abstract void DrawAxisLabel (GraphView graph, int screenPosition, string text);
/// Draws labels and axis ticks
///
public abstract void DrawAxisLabels (GraphView graph);
/// Draws the solid line of the axis
///
public abstract void DrawAxisLine (GraphView graph);
/// Resets all configurable properties of the axis to default values
public virtual void Reset ()
{
Increment = 1;
ShowLabelsEvery = DefaultShowLabelsEvery;
Visible = true;
Text = "";
LabelGetter = DefaultLabelGetter;
Minimum = null;
}
/// Draws a single cell of the solid line of the axis
///
///
///
protected abstract void DrawAxisLine (GraphView graph, int x, int y);
private string DefaultLabelGetter (AxisIncrementToRender toRender) { return toRender.Value.ToString ("N0"); }
}
/// The horizontal (x-axis) of a
public class HorizontalAxis : Axis
{
///
/// Creates a new instance of axis with an of
///
public HorizontalAxis () : base (Orientation.Horizontal) { }
///
/// Draws the given on the axis at x . For the screen y
/// position use
///
/// Graph being drawn onto
/// Number of screen columns along the axis to take before rendering
/// Text to render under the axis tick
public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
{
ConsoleDriver driver = Application.Driver;
int y = GetAxisYPosition (graph);
graph.Move (screenPosition, y);
// draw the tick on the axis
Application.Driver?.AddRune (Glyphs.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.Viewport.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.Viewport.Height - 1));
driver.AddStr (toRender);
}
}
/// Draws the horizontal x-axis labels and ticks
public override void DrawAxisLabels (GraphView graph)
{
if (!Visible || Increment == 0)
{
return;
}
Rectangle viewport = graph.Viewport;
IEnumerable labels = GetLabels (graph, viewport);
foreach (AxisIncrementToRender 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.Viewport.Width)
{
toRender = toRender.Substring (0, graph.Viewport.Width);
}
graph.Move (graph.Viewport.Width / 2 - toRender.Length / 2, graph.Viewport.Height - 1);
Application.Driver?.AddStr (toRender);
}
}
/// Draws the horizontal axis line
///
public override void DrawAxisLine (GraphView graph)
{
if (!Visible)
{
return;
}
Rectangle bounds = graph.Viewport;
graph.Move (0, 0);
int 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 minimum (minimum is in graph space units)
if (Minimum.HasValue)
{
// start at the screen location of the minimum
int 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);
}
}
///
/// 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.
///
///
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
Point 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.Viewport.Height - ((int)graph.MarginBottom + 1));
}
/// Draws a horizontal axis line at the given , screen coordinates
///
///
///
protected override void DrawAxisLine (GraphView graph, int x, int y)
{
graph.Move (x, y);
Application.Driver?.AddRune (Glyphs.HLine);
}
private IEnumerable GetLabels (GraphView graph, Rectangle viewport)
{
// if no labels
if (Increment == 0)
{
yield break;
}
var labels = 0;
int y = GetAxisYPosition (graph);
RectangleF start = graph.ScreenToGraphSpace ((int)graph.MarginLeft, y);
RectangleF end = graph.ScreenToGraphSpace (viewport.Width, y);
// don't draw labels below the minimum
if (Minimum.HasValue)
{
start.X = Math.Max (start.X, Minimum.Value);
}
RectangleF current = start;
while (current.X < end.X)
{
int screenX = graph.GraphSpaceToScreen (new PointF (current.X, current.Y)).X;
// 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 also needs a label
if (labels++ % ShowLabelsEvery == 0)
{
toRender.Text = LabelGetter (toRender);
}
;
}
// Label or no label definitely render it
yield return toRender;
current.X += Increment;
}
}
}
/// The vertical (i.e. Y axis) of a
public class VerticalAxis : Axis
{
/// Creates a new axis
public VerticalAxis () : base (Orientation.Vertical) { }
///
/// Draws the given on the axis at y . For the screen x
/// position use
///
/// Graph being drawn onto
/// Number of rows from the top of the screen (i.e. down the axis) before rendering
///
/// Text to render to the left of the axis tick. Ensure to set or
/// sufficient that it is visible
///
public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
{
int x = GetAxisXPosition (graph);
int labelThickness = text.Length;
graph.Move (x, screenPosition);
// draw the tick on the axis
Application.Driver?.AddRune (Glyphs.RightTee);
// and the label text
if (!string.IsNullOrWhiteSpace (text))
{
graph.Move (Math.Max (0, x - labelThickness), screenPosition);
Application.Driver?.AddStr (text);
}
}
/// Draws axis markers and labels
///
public override void DrawAxisLabels (GraphView graph)
{
if (!Visible || Increment == 0)
{
return;
}
Rectangle bounds = graph.Viewport;
IEnumerable labels = GetLabels (graph, bounds);
foreach (AxisIncrementToRender 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.Viewport.Height)
{
toRender = toRender.Substring (0, graph.Viewport.Height);
}
// Draw it 1 letter at a time vertically down row 0 of the control
int startDrawingAtY = graph.Viewport.Height / 2 - toRender.Length / 2;
for (var i = 0; i < toRender.Length; i++)
{
graph.Move (0, startDrawingAtY + i);
Application.Driver?.AddRune ((Rune)toRender [i]);
}
}
}
/// Draws the vertical axis line
///
public override void DrawAxisLine (GraphView graph)
{
if (!Visible)
{
return;
}
Rectangle bounds = graph.Viewport;
int x = GetAxisXPosition (graph);
int 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 (var i = 0; i < yEnd; i++)
{
DrawAxisLine (graph, x, i);
}
}
///
/// 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.
///
///
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
Point 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.Viewport.Width - 1);
}
/// Draws a vertical axis line at the given , screen coordinates
///
///
///
protected override void DrawAxisLine (GraphView graph, int x, int y)
{
graph.Move (x, y);
Application.Driver?.AddRune (Glyphs.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.Viewport.Height;
}
private IEnumerable GetLabels (GraphView graph, Rectangle bounds)
{
// if no labels
if (Increment == 0)
{
yield break;
}
var 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
RectangleF start = graph.ScreenToGraphSpace (x, bounds.Height - (1 + (int)graph.MarginBottom));
RectangleF end = graph.ScreenToGraphSpace (x, 0);
// don't draw labels below the minimum
if (Minimum.HasValue)
{
start.Y = Math.Max (start.Y, Minimum.Value);
}
RectangleF current = start;
while (current.Y < end.Y)
{
int screenY = graph.GraphSpaceToScreen (new PointF (current.X, current.Y)).Y;
// 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;
}
}
}
/// A location on an axis of a that may or may not have a label associated with it
public class AxisIncrementToRender
{
private string _text = "";
/// Describe a new section of an axis that requires an axis increment symbol and/or label
///
///
///
public AxisIncrementToRender (Orientation orientation, int screen, float value)
{
Orientation = orientation;
ScreenLocation = screen;
Value = value;
}
/// Direction of the parent axis
public Orientation Orientation { get; }
/// The screen location (X or Y depending on ) that the increment will be rendered at
public int ScreenLocation { get; }
/// The value at this position on the axis in graph space
public float Value { get; }
/// The text (if any) that should be displayed at this axis increment
///
internal string Text
{
get => _text;
set => _text = value ?? "";
}
}
/// Delegate for custom formatting of axis labels. Determines what should be displayed at a given label
/// The axis increment to which the label is attached
///
public delegate string LabelGetterDelegate (AxisIncrementToRender toRender);