Add breakdown chart support

This also cleans up the bar chart code slightly and fixes
some minor bugs that were detected in related code.

Closes #244
This commit is contained in:
Patrik Svensson
2021-01-31 22:46:15 +01:00
committed by Patrik Svensson
parent 58400fe74e
commit b64e016e8c
32 changed files with 911 additions and 41 deletions

View File

@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A renderable (horizontal) bar chart.
/// </summary>
public sealed class BarChart : Renderable, IHasCulture
{
/// <summary>
/// Gets the bar chart data.
/// </summary>
public List<IBarChartItem> Data { get; }
/// <summary>
/// Gets or sets the width of the bar chart.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets the bar chart label.
/// </summary>
public string? Label { get; set; }
/// <summary>
/// Gets or sets the bar chart label alignment.
/// </summary>
public Justify? LabelAlignment { get; set; } = Justify.Center;
/// <summary>
/// Gets or sets a value indicating whether or not
/// values should be shown next to each bar.
/// </summary>
public bool ShowValues { get; set; } = true;
/// <summary>
/// Gets or sets the culture that's used to format values.
/// </summary>
/// <remarks>Defaults to invariant culture.</remarks>
public CultureInfo? Culture { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BarChart"/> class.
/// </summary>
public BarChart()
{
Data = new List<IBarChartItem>();
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var maxValue = Data.Max(item => item.Value);
var grid = new Grid();
grid.Collapse();
grid.AddColumn(new GridColumn().PadRight(2).RightAligned());
grid.AddColumn(new GridColumn().PadLeft(0));
grid.Width = width;
if (!string.IsNullOrWhiteSpace(Label))
{
grid.AddRow(Text.Empty, new Markup(Label).Alignment(LabelAlignment));
}
foreach (var item in Data)
{
grid.AddRow(
new Markup(item.Label),
new ProgressBar()
{
Value = item.Value,
MaxValue = maxValue,
ShowRemaining = false,
CompletedStyle = new Style().Foreground(item.Color ?? Color.Default),
FinishedStyle = new Style().Foreground(item.Color ?? Color.Default),
UnicodeBar = '█',
AsciiBar = '█',
ShowValue = ShowValues,
Culture = Culture,
});
}
return ((IRenderable)grid).Render(context, width);
}
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// An item that's shown in a bar chart.
/// </summary>
public sealed class BarChartItem : IBarChartItem
{
/// <summary>
/// Gets the item label.
/// </summary>
public string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
public double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
public Color? Color { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BarChartItem"/> class.
/// </summary>
/// <param name="label">The item label.</param>
/// <param name="value">The item value.</param>
/// <param name="color">The item color.</param>
public BarChartItem(string label, double value, Color? color = null)
{
Label = label ?? throw new ArgumentNullException(nameof(label));
Value = value;
Color = color;
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal sealed class BreakdownBar : Renderable
{
private readonly List<IBreakdownChartItem> _data;
public int? Width { get; set; }
public BreakdownBar(List<IBreakdownChartItem> data)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
}
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
// Chart
var maxValue = _data.Sum(i => i.Value);
var items = _data.ToArray();
var bars = Ratio.Distribute(width, items.Select(i => Math.Max(0, (int)(width * (i.Value / maxValue)))).ToArray());
for (var index = 0; index < items.Length; index++)
{
yield return new Segment(new string('█', bars[index]), new Style(items[index].Color));
}
yield return Segment.LineBreak;
}
}
}

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A renderable breakdown chart.
/// </summary>
public sealed class BreakdownChart : Renderable, IHasCulture
{
/// <summary>
/// Gets the breakdown chart data.
/// </summary>
public List<IBreakdownChartItem> Data { get; }
/// <summary>
/// Gets or sets the width of the breakdown chart.
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not
/// to show values as percentages or not.
/// </summary>
public bool ShowAsPercentages { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not to show tags.
/// </summary>
public bool ShowTags { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not to show tag values.
/// </summary>
public bool ShowTagValues { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the
/// chart and tags should be rendered in compact mode.
/// </summary>
public bool Compact { get; set; } = true;
/// <summary>
/// Gets or sets the <see cref="CultureInfo"/> to use
/// when rendering values.
/// </summary>
/// <remarks>Defaults to invariant culture.</remarks>
public CultureInfo? Culture { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BreakdownChart"/> class.
/// </summary>
public BreakdownChart()
{
Data = new List<IBreakdownChartItem>();
Culture = CultureInfo.InvariantCulture;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var grid = new Grid().Width(width);
grid.AddColumn(new GridColumn().NoWrap());
// Bar
grid.AddRow(new BreakdownBar(Data)
{
Width = width,
});
if (ShowTags)
{
if (!Compact)
{
grid.AddEmptyRow();
}
// Tags
grid.AddRow(new BreakdownTags(Data)
{
Width = width,
Culture = Culture,
ShowPercentages = ShowAsPercentages,
ShowTagValues = ShowTagValues,
});
}
return ((IRenderable)grid).Render(context, width);
}
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// An item that's shown in a breakdown chart.
/// </summary>
public sealed class BreakdownChartItem : IBreakdownChartItem
{
/// <summary>
/// Gets the item label.
/// </summary>
public string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
public double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
public Color Color { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BreakdownChartItem"/> class.
/// </summary>
/// <param name="label">The item label.</param>
/// <param name="value">The item value.</param>
/// <param name="color">The item color.</param>
public BreakdownChartItem(string label, double value, Color color)
{
Label = label ?? throw new ArgumentNullException(nameof(label));
Value = value;
Color = color;
}
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal sealed class BreakdownTags : Renderable
{
private readonly List<IBreakdownChartItem> _data;
public int? Width { get; set; }
public CultureInfo? Culture { get; set; }
public bool ShowPercentages { get; set; }
public bool ShowTagValues { get; set; } = true;
public BreakdownTags(List<IBreakdownChartItem> data)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
}
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(width, width);
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var panels = new List<Panel>();
foreach (var item in _data)
{
var panel = new Panel(GetTag(item, culture));
panel.Inline = true;
panel.Padding = new Padding(0, 0);
panel.NoBorder();
panels.Add(panel);
}
foreach (var segment in ((IRenderable)new Columns(panels).Padding(0, 0)).Render(context, maxWidth))
{
yield return segment;
}
}
private string GetTag(IBreakdownChartItem item, CultureInfo culture)
{
return string.Format(
culture, "[{0}]■[/] {1}",
item.Color.ToMarkup() ?? "default",
FormatValue(item, culture)).Trim();
}
private string FormatValue(IBreakdownChartItem item, CultureInfo culture)
{
if (ShowTagValues)
{
return string.Format(culture, "{0} [grey]{1}{2}[/]",
item.Label.EscapeMarkup(), item.Value,
ShowPercentages ? "%" : string.Empty);
}
return item.Label.EscapeMarkup();
}
}
}

View File

@ -0,0 +1,23 @@
namespace Spectre.Console
{
/// <summary>
/// Represents a bar chart item.
/// </summary>
public interface IBarChartItem
{
/// <summary>
/// Gets the item label.
/// </summary>
string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
Color? Color { get; }
}
}

View File

@ -0,0 +1,23 @@
namespace Spectre.Console
{
/// <summary>
/// Represents a breakdown chart item.
/// </summary>
public interface IBreakdownChartItem
{
/// <summary>
/// Gets the item label.
/// </summary>
string Label { get; }
/// <summary>
/// Gets the item value.
/// </summary>
double Value { get; }
/// <summary>
/// Gets the item color.
/// </summary>
Color Color { get; }
}
}