mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-01-01 08:50:25 +01:00
Fixes #2981. LegendAnnotation: 'Frame.DrawFrame(Rect, bool)' is obsolete: 'This method is obsolete in v2. Use use LineCanvas or Frame (#2982)
* Fixes #2981. LegendAnnotation: 'Frame.DrawFrame(Rect, bool)' is obsolete: 'This method is obsolete in v2. Use use LineCanvas or Frame * Fixes #2983. View need a alternative DrawFrame for the v2. * Use new DrawFrame method. * Add a view for the legend annotations. * Prefix with underscore. * Change LegendAnnotation class to derived from View. * The Bounds isn't needed, it's enough to set the Pos/Dim. * Fix unit test to differentiate Bounds from Frame. * Add DrawIncompleteFrame method and unit tests. * Add more unit tests to LineCanvas. * Fix newline conflict errors. * Add DrawIncompleteFrame method and unit tests. * Add more unit tests to LineCanvas. * Fix newline conflict errors. * I will never rely on zero-location-based unit test again. * Fix TestTreeViewColor unit test fail. * Add BorderStyle option to the menu. * Revert "I will never rely on zero-location-based unit test again." This reverts commit62adf6f285. * Revert "Fix newline conflict errors." This reverts commit4acf72612d. * Revert "Add more unit tests to LineCanvas." This reverts commit66bc6f514e. * Revert "Add DrawIncompleteFrame method and unit tests." This reverts commit680ba264e1. * Revert "Fixes #2983. View need a alternative DrawFrame for the v2." This reverts commitdade9fd767. * Removed resharper settings from editorconfig * Rename to OnDrawAdornments. * Added diagnostics as a double-check. Code cleanup. API doc improvements. * Fix typo for retest again. * Increase the graph size. --------- Co-authored-by: Tig <tig@users.noreply.github.com> Co-authored-by: Tig Kindel <tig@kindel.com>
This commit is contained in:
@@ -77,7 +77,7 @@ public class Border : Adornment {
|
||||
/// setting the <see cref="Thickness"/> to <c>(1,1,1,1)</c> and setting the line style of the
|
||||
/// views that comprise the border. If set to <see cref="LineStyle.None"/> no border will be drawn.
|
||||
/// </summary>
|
||||
public new LineStyle LineStyle {
|
||||
public LineStyle LineStyle {
|
||||
get {
|
||||
if (_lineStyle.HasValue) {
|
||||
return _lineStyle.Value;
|
||||
|
||||
@@ -211,7 +211,7 @@ public partial class View {
|
||||
/// This internal method is overridden by Adornment to do nothing to prevent recursion during View construction.
|
||||
/// And, because Adornments don't have Adornments. It's internal to support unit tests.
|
||||
/// </summary>
|
||||
/// <param name="adornment"></param>
|
||||
/// <param name="adornmentType"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
internal virtual Adornment CreateAdornment (Type adornmentType)
|
||||
|
||||
@@ -114,62 +114,55 @@ namespace Terminal.Gui {
|
||||
/// A box containing symbol definitions e.g. meanings for colors in a graph.
|
||||
/// The 'Key' to the graph
|
||||
/// </summary>
|
||||
public class LegendAnnotation : IAnnotation {
|
||||
|
||||
public class LegendAnnotation : View, 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
|
||||
/// Returns false i.e. Legends 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>> ();
|
||||
List<Tuple<GraphCellToRender, string>> _entries = new List<Tuple<GraphCellToRender, string>> ();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new empty legend at the given screen coordinates
|
||||
/// Creates a new empty legend at the empty screen coordinates.
|
||||
/// </summary>
|
||||
public LegendAnnotation () : this (Rect.Empty) { }
|
||||
|
||||
/// <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;
|
||||
X = legendBounds.X;
|
||||
Y = legendBounds.Y;
|
||||
Width = legendBounds.Width;
|
||||
Height = legendBounds.Height;
|
||||
BorderStyle = LineStyle.Single;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the Legend and all entries into the area within <see cref="Bounds"/>
|
||||
/// Draws the Legend and all entries into the area within <see cref="View.Bounds"/>
|
||||
/// </summary>
|
||||
/// <param name="graph"></param>
|
||||
public void Render (GraphView graph)
|
||||
{
|
||||
if (Border) {
|
||||
graph.Border.DrawFrame (Bounds, true);
|
||||
if (!IsInitialized) {
|
||||
ColorScheme = new ColorScheme () { Normal = Application.Driver.GetAttribute () };
|
||||
graph.Add (this);
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (BorderStyle != LineStyle.None) {
|
||||
OnDrawAdornments ();
|
||||
OnRenderLineCanvas ();
|
||||
}
|
||||
|
||||
int linesDrawn = 0;
|
||||
|
||||
foreach (var entry in entries) {
|
||||
foreach (var entry in _entries) {
|
||||
|
||||
if (entry.Item1.Color.HasValue) {
|
||||
Application.Driver.SetAttribute (entry.Item1.Color.Value);
|
||||
@@ -178,35 +171,35 @@ namespace Terminal.Gui {
|
||||
}
|
||||
|
||||
// add the symbol
|
||||
graph.AddRune (x, y + linesDrawn, entry.Item1.Rune);
|
||||
AddRune (0, linesDrawn, entry.Item1.Rune);
|
||||
|
||||
// switch to normal coloring (for the text)
|
||||
graph.SetDriverColorToGraphColor ();
|
||||
|
||||
// add the text
|
||||
graph.Move (x + 1, y + linesDrawn);
|
||||
Move (1, linesDrawn);
|
||||
|
||||
string str = TextFormatter.ClipOrPad (entry.Item2, availableWidth - 1);
|
||||
string str = TextFormatter.ClipOrPad (entry.Item2, Bounds.Width - 1);
|
||||
Application.Driver.AddStr (str);
|
||||
|
||||
linesDrawn++;
|
||||
|
||||
|
||||
// Legend has run out of space
|
||||
if (linesDrawn >= availableHeight) {
|
||||
if (linesDrawn >= Bounds.Height) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an entry into the legend. Duplicate entries are permissable
|
||||
/// Adds an entry into the legend. Duplicate entries are permissible
|
||||
/// </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>
|
||||
/// if outside of Legend <see cref="View.Bounds"/></param>
|
||||
public void AddEntry (GraphCellToRender graphCellToRender, string text)
|
||||
{
|
||||
entries.Add (Tuple.Create (graphCellToRender, text));
|
||||
_entries.Add (Tuple.Create (graphCellToRender, text));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,321 +1,330 @@
|
||||
using System.Text;
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// View for rendering graphs (bar, scatter, etc...).
|
||||
/// </summary>
|
||||
public class GraphView : View {
|
||||
|
||||
#nullable enable
|
||||
namespace Terminal.Gui {
|
||||
/// <summary>
|
||||
/// Control for rendering graphs (bar, scatter etc)
|
||||
/// Creates a new graph with a 1 to 1 graph space with absolute layout.
|
||||
/// </summary>
|
||||
public class GraphView : View {
|
||||
public GraphView ()
|
||||
{
|
||||
CanFocus = true;
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal axis
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public HorizontalAxis AxisX { get; set; }
|
||||
AxisX = new HorizontalAxis ();
|
||||
AxisY = new VerticalAxis ();
|
||||
|
||||
/// <summary>
|
||||
/// Vertical axis
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public VerticalAxis AxisY { get; set; }
|
||||
// Things this view knows how to do
|
||||
AddCommand (Command.ScrollUp, () => {
|
||||
Scroll (0, CellSize.Y);
|
||||
return true;
|
||||
});
|
||||
AddCommand (Command.ScrollDown, () => {
|
||||
Scroll (0, -CellSize.Y);
|
||||
return true;
|
||||
});
|
||||
AddCommand (Command.ScrollRight, () => {
|
||||
Scroll (CellSize.X, 0);
|
||||
return true;
|
||||
});
|
||||
AddCommand (Command.ScrollLeft, () => {
|
||||
Scroll (-CellSize.X, 0);
|
||||
return true;
|
||||
});
|
||||
AddCommand (Command.PageUp, () => {
|
||||
PageUp ();
|
||||
return true;
|
||||
});
|
||||
AddCommand (Command.PageDown, () => {
|
||||
PageDown ();
|
||||
return true;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Collection of data series that are rendered in the graph
|
||||
/// </summary>
|
||||
public List<ISeries> Series { get; } = new List<ISeries> ();
|
||||
KeyBindings.Add (KeyCode.CursorRight, Command.ScrollRight);
|
||||
KeyBindings.Add (KeyCode.CursorLeft, Command.ScrollLeft);
|
||||
KeyBindings.Add (KeyCode.CursorUp, Command.ScrollUp);
|
||||
KeyBindings.Add (KeyCode.CursorDown, Command.ScrollDown);
|
||||
|
||||
/// <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 ();
|
||||
|
||||
// Things this view knows how to do
|
||||
AddCommand (Command.ScrollUp, () => { Scroll (0, CellSize.Y); return true; });
|
||||
AddCommand (Command.ScrollDown, () => { Scroll (0, -CellSize.Y); return true; });
|
||||
AddCommand (Command.ScrollRight, () => { Scroll (CellSize.X, 0); return true; });
|
||||
AddCommand (Command.ScrollLeft, () => { Scroll (-CellSize.X, 0); return true; });
|
||||
AddCommand (Command.PageUp, () => { PageUp (); return true; });
|
||||
AddCommand (Command.PageDown, () => { PageDown (); return true; });
|
||||
|
||||
KeyBindings.Add (KeyCode.CursorRight, Command.ScrollRight);
|
||||
KeyBindings.Add (KeyCode.CursorLeft, Command.ScrollLeft);
|
||||
KeyBindings.Add (KeyCode.CursorUp, Command.ScrollUp);
|
||||
KeyBindings.Add (KeyCode.CursorDown, Command.ScrollDown);
|
||||
|
||||
// Not bound by default (preserves backwards compatibility)
|
||||
//KeyBindings.Add (Key.PageUp, Command.PageUp);
|
||||
//KeyBindings.Add (Key.PageDown, Command.PageDown);
|
||||
}
|
||||
|
||||
/// <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 OnDrawContent (Rect contentArea)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// The drawable area of the graph (anything that isn't in the margins)
|
||||
var graphScreenWidth = Bounds.Width - ((int)MarginLeft);
|
||||
var graphScreenHeight = Bounds.Height - (int)MarginBottom;
|
||||
|
||||
// if the margins take up the full draw bounds don't render
|
||||
if (graphScreenWidth < 0 || graphScreenHeight < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw 'before' annotations
|
||||
foreach (var a in Annotations.ToArray ().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, (Rune)'\u253C');
|
||||
}
|
||||
|
||||
SetDriverColorToGraphColor ();
|
||||
|
||||
Rect drawBounds = new Rect ((int)MarginLeft, 0, graphScreenWidth, graphScreenHeight);
|
||||
|
||||
RectangleF graphSpace = ScreenToGraphSpace (drawBounds);
|
||||
|
||||
foreach (var s in Series.ToArray ()) {
|
||||
|
||||
s.DrawSeries (this, drawBounds, graphSpace);
|
||||
|
||||
// If a series changes the graph color reset it
|
||||
SetDriverColorToGraphColor ();
|
||||
}
|
||||
|
||||
SetDriverColorToGraphColor ();
|
||||
|
||||
// Draw 'after' annotations
|
||||
foreach (var a in Annotations.ToArray ().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 ?? (GetNormalColor ()));
|
||||
}
|
||||
|
||||
/// <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/>
|
||||
/// <remarks>Also ensures that cursor is invisible after entering the <see cref="GraphView"/>.</remarks>
|
||||
public override bool OnEnter (View view)
|
||||
{
|
||||
Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
return base.OnEnter (view);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls the graph up 1 page
|
||||
/// </summary>
|
||||
public void PageUp ()
|
||||
{
|
||||
Scroll (0, CellSize.Y * Bounds.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls the graph down 1 page
|
||||
/// </summary>
|
||||
public void PageDown ()
|
||||
{
|
||||
Scroll (0, -1 * CellSize.Y * Bounds.Height);
|
||||
}
|
||||
/// <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>
|
||||
public 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
|
||||
// Not bound by default (preserves backwards compatibility)
|
||||
//KeyBindings.Add (Key.PageUp, Command.PageUp);
|
||||
//KeyBindings.Add (Key.PageDown, Command.PageDown);
|
||||
}
|
||||
|
||||
/// <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 ();
|
||||
|
||||
/// <summary>
|
||||
/// Elements drawn into graph after series have been drawn e.g. Legends etc.
|
||||
/// </summary>
|
||||
public List<IAnnotation> Annotations { get; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Amount of space to leave on left of the graph. Graph content (<see cref="Series"/>)
|
||||
/// will not be rendered in margins but axis labels may be. Use <see cref="Padding"/> to
|
||||
/// add a margin outside of the GraphView.
|
||||
/// </summary>
|
||||
public uint MarginLeft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Amount of space to leave on bottom of the graph. Graph content (<see cref="Series"/>)
|
||||
/// will not be rendered in margins but axis labels may be. Use <see cref="Padding"/> to
|
||||
/// add a margin outside of the GraphView.
|
||||
/// </summary>
|
||||
public uint MarginBottom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The graph space position of the bottom left of the graph.
|
||||
/// Changing this scrolls the viewport around in the graph.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public PointF ScrollOffset { get; set; } = new (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 (1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// The color of the background of the graph and axis/labels.
|
||||
/// </summary>
|
||||
public Attribute? GraphColor { get; set; }
|
||||
|
||||
/// <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 OnDrawContent (Rect contentArea)
|
||||
{
|
||||
if (CellSize.X == 0 || CellSize.Y == 0) {
|
||||
throw new Exception ($"{nameof (CellSize)} cannot be 0");
|
||||
}
|
||||
|
||||
SetDriverColorToGraphColor ();
|
||||
|
||||
Move (0, 0);
|
||||
|
||||
// clear all old content
|
||||
for (var 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;
|
||||
}
|
||||
|
||||
// The drawable area of the graph (anything that isn't in the margins)
|
||||
var graphScreenWidth = Bounds.Width - (int)MarginLeft;
|
||||
var graphScreenHeight = Bounds.Height - (int)MarginBottom;
|
||||
|
||||
// if the margins take up the full draw bounds don't render
|
||||
if (graphScreenWidth < 0 || graphScreenHeight < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw 'before' annotations
|
||||
foreach (var a in Annotations.ToArray ().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, (Rune)'\u253C');
|
||||
}
|
||||
|
||||
SetDriverColorToGraphColor ();
|
||||
|
||||
var drawBounds = new Rect ((int)MarginLeft, 0, graphScreenWidth, graphScreenHeight);
|
||||
|
||||
var graphSpace = ScreenToGraphSpace (drawBounds);
|
||||
|
||||
foreach (var s in Series.ToArray ()) {
|
||||
|
||||
s.DrawSeries (this, drawBounds, graphSpace);
|
||||
|
||||
// If a series changes the graph color reset it
|
||||
SetDriverColorToGraphColor ();
|
||||
}
|
||||
|
||||
SetDriverColorToGraphColor ();
|
||||
|
||||
// Draw 'after' annotations
|
||||
foreach (var a in Annotations.ToArray ().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 ?? GetNormalColor ());
|
||||
|
||||
/// <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) => new (
|
||||
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 may 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 content area of the view.
|
||||
/// </returns>
|
||||
public Point GraphSpaceToScreen (PointF location) => new (
|
||||
(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/>
|
||||
/// <remarks>Also ensures that cursor is invisible after entering the <see cref="GraphView"/>.</remarks>
|
||||
public override bool OnEnter (View view)
|
||||
{
|
||||
Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
||||
return base.OnEnter (view);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls the graph up 1 page.
|
||||
/// </summary>
|
||||
public void PageUp () => Scroll (0, CellSize.Y * Bounds.Height);
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls the graph down 1 page.
|
||||
/// </summary>
|
||||
public void PageDown () => Scroll (0, -1 * CellSize.Y * Bounds.Height);
|
||||
|
||||
/// <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>
|
||||
public 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
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
var x0 = start.X;
|
||||
var y0 = start.Y;
|
||||
var x1 = end.X;
|
||||
var 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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1252,6 +1252,26 @@ namespace Terminal.Gui.ViewsTests {
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructors_Defaults ()
|
||||
{
|
||||
var legend = new LegendAnnotation ();
|
||||
Assert.Equal (Rect.Empty, legend.Bounds);
|
||||
Assert.Equal (Rect.Empty, legend.Frame);
|
||||
Assert.Equal (LineStyle.Single, legend.BorderStyle);
|
||||
Assert.False (legend.BeforeSeries);
|
||||
|
||||
var bounds = new Rect (1, 2, 10, 3);
|
||||
legend = new LegendAnnotation (bounds);
|
||||
Assert.Equal (new Rect (0, 0, 8, 1), legend.Bounds);
|
||||
Assert.Equal (bounds, legend.Frame);
|
||||
Assert.Equal (LineStyle.Single, legend.BorderStyle);
|
||||
Assert.False (legend.BeforeSeries);
|
||||
legend.BorderStyle = LineStyle.None;
|
||||
Assert.Equal (new Rect (0, 0, 10, 3), legend.Bounds);
|
||||
Assert.Equal (bounds, legend.Frame);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegendNormalUsage_WithBorder ()
|
||||
{
|
||||
@@ -1286,7 +1306,7 @@ namespace Terminal.Gui.ViewsTests {
|
||||
legend.AddEntry (new GraphCellToRender ((Rune)'B'), "?"); // this will exercise pad
|
||||
legend.AddEntry (new GraphCellToRender ((Rune)'C'), "Cat");
|
||||
legend.AddEntry (new GraphCellToRender ((Rune)'H'), "Hattter"); // not enough space for this oen
|
||||
legend.Border = false;
|
||||
legend.BorderStyle = LineStyle.None;
|
||||
|
||||
gv.Annotations.Add (legend);
|
||||
gv.Draw ();
|
||||
|
||||
Reference in New Issue
Block a user