namespace Terminal.Gui;
///
/// Describes an overlay element that is rendered either before or after a series.
///
/// Annotations can be positioned either in screen space (e.g. a legend) or in graph space (e.g. a line showing
/// high point)
///
/// Unlike , annotations are allowed to draw into graph margins
///
public interface IAnnotation
{
///
/// True if annotation should be drawn before . This allows Series and later annotations to
/// potentially draw over the top of this annotation.
///
bool BeforeSeries { get; }
///
/// Called once after series have been rendered (or before if is true). Use
/// to draw and to avoid drawing outside of graph
///
///
void Render (GraphView graph);
}
/// Displays text at a given position (in screen space or graph space)
public class TextAnnotation : IAnnotation
{
///
/// The location in graph space to draw the . This annotation will only show if the point is in
/// the current viewable area of the graph presented in the
///
public PointF GraphPosition { get; set; }
///
/// The location on screen to draw the regardless of scroll/zoom settings. This overrides
/// if specified.
///
public Point? ScreenPosition { get; set; }
/// Text to display on the graph
public string Text { get; set; }
/// True to add text before plotting series. Defaults to false
public bool BeforeSeries { get; set; }
/// Draws the annotation
///
public void Render (GraphView graph)
{
if (ScreenPosition.HasValue)
{
DrawText (graph, ScreenPosition.Value.X, ScreenPosition.Value.Y);
return;
}
Point screenPos = graph.GraphSpaceToScreen (GraphPosition);
DrawText (graph, screenPos.X, screenPos.Y);
}
///
/// Draws the at the given coordinates with truncation to avoid spilling over
/// of the
///
///
/// Screen x position to start drawing string
/// Screen y position to start drawing string
protected void DrawText (GraphView graph, int x, int y)
{
// the draw point is out of control bounds
if (!graph.Viewport.Contains (new Point (x, y)))
{
return;
}
// There is no text to draw
if (string.IsNullOrWhiteSpace (Text))
{
return;
}
graph.Move (x, y);
int availableWidth = graph.Viewport.Width - x;
if (availableWidth <= 0)
{
return;
}
if (Text.Length < availableWidth)
{
View.Driver.AddStr (Text);
}
else
{
View.Driver.AddStr (Text.Substring (0, availableWidth));
}
}
}
/// A box containing symbol definitions e.g. meanings for colors in a graph. The 'Key' to the graph
public class LegendAnnotation : View, IAnnotation
{
/// Ordered collection of entries that are rendered in the legend.
private readonly List> _entries = new ();
/// Creates a new empty legend at the empty screen coordinates.
public LegendAnnotation () : this (Rectangle.Empty) { }
/// Creates a new empty legend at the given screen coordinates.
///
/// Defines the area available for the legend to render in (within the graph). This is in
/// screen units (i.e. not graph space)
///
public LegendAnnotation (Rectangle legendBounds)
{
X = legendBounds.X;
Y = legendBounds.Y;
Width = legendBounds.Width;
Height = legendBounds.Height;
BorderStyle = LineStyle.Single;
}
/// Returns false i.e. Legends render after series
public bool BeforeSeries => false;
// BUGBUG: Legend annotations are subviews. But for some reason the are rendered directly in OnDrawContent
// BUGBUG: instead of just being normal subviews. They get rendered as blank rects and thus we disable subview drawing.
///
protected override bool OnDrawingText () { return true; }
// BUGBUG: Legend annotations are subviews. But for some reason the are rendered directly in OnDrawContent
// BUGBUG: instead of just being normal subviews. They get rendered as blank rects and thus we disable subview drawing.
///
protected override bool OnClearingViewport () { return true; }
/// Draws the Legend and all entries into the area within
///
public void Render (GraphView graph)
{
if (!IsInitialized)
{
ColorScheme = new ColorScheme { Normal = Application.Driver?.GetAttribute () ?? Attribute.Default};
graph.Add (this);
}
if (BorderStyle != LineStyle.None)
{
DrawAdornments ();
RenderLineCanvas ();
}
var linesDrawn = 0;
foreach (Tuple entry in _entries)
{
if (entry.Item1.Color.HasValue)
{
SetAttribute (entry.Item1.Color.Value);
}
else
{
graph.SetDriverColorToGraphColor ();
}
// add the symbol
AddRune (0, linesDrawn, entry.Item1.Rune);
// switch to normal coloring (for the text)
graph.SetDriverColorToGraphColor ();
// add the text
Move (1, linesDrawn);
string str = TextFormatter.ClipOrPad (entry.Item2, Viewport.Width - 1);
Application.Driver?.AddStr (str);
linesDrawn++;
// Legend has run out of space
if (linesDrawn >= Viewport.Height)
{
break;
}
}
}
/// Adds an entry into the legend. Duplicate entries are permissible
/// The symbol appearing on the graph that should appear in the legend
///
/// Text to render on this line of the legend. Will be truncated if outside of Legend
///
///
public void AddEntry (GraphCellToRender graphCellToRender, string text) { _entries.Add (Tuple.Create (graphCellToRender, text)); }
}
/// Sequence of lines to connect points e.g. of a
public class PathAnnotation : IAnnotation
{
/// Color for the line that connects points
public Attribute? LineColor { get; set; }
/// The symbol that gets drawn along the line, defaults to '.'
public Rune LineRune { get; set; } = new ('.');
/// Points that should be connected. Lines will be drawn between points in the order they appear in the list
public List Points { get; set; } = new ();
/// True to add line before plotting series. Defaults to false
public bool BeforeSeries { get; set; }
/// Draws lines connecting each of the
///
public void Render (GraphView graph)
{
graph.SetAttribute (LineColor ?? graph.ColorScheme.Normal);
foreach (LineF line in PointsToLines ())
{
Point start = graph.GraphSpaceToScreen (line.Start);
Point end = graph.GraphSpaceToScreen (line.End);
graph.DrawLine (start, end, LineRune);
}
}
/// Generates lines joining
///
private IEnumerable PointsToLines ()
{
for (var i = 0; i < Points.Count - 1; i++)
{
yield return new LineF (Points [i], Points [i + 1]);
}
}
}