From 7dccb310f35d9b7f96d517c7d9298567025fc520 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Tue, 22 Dec 2020 22:26:17 +0100 Subject: [PATCH] Add support for bar charts Closes #103 --- docs/input/assets/images/barchart.png | Bin 0 -> 3533 bytes docs/input/widgets/barchart.md | 75 ++++++ docs/input/widgets/calendar.md | 2 +- docs/input/widgets/canvas-image.md | 2 +- docs/input/widgets/canvas.md | 2 +- docs/input/widgets/figlet.md | 2 +- docs/input/widgets/rule.md | 2 +- examples/Charts/Charts.csproj | 15 ++ examples/Charts/Program.cs | 21 ++ ...Tests.Should_Render_Correctly.verified.txt | 4 + .../Unit/BarChartTests.cs | 28 +++ src/Spectre.Console.sln | 15 ++ .../Extensions/BarGraphExtensions.cs | 234 ++++++++++++++++++ .../Extensions/GridExtensions.cs | 17 ++ .../Extensions/TableExtensions.cs | 2 +- src/Spectre.Console/Rendering/Renderable.cs | 3 + src/Spectre.Console/Widgets/BarChart.cs | 82 ++++++ src/Spectre.Console/Widgets/BarChartItem.cs | 38 +++ src/Spectre.Console/Widgets/Grid.cs | 6 + src/Spectre.Console/Widgets/IBarGraphItem.cs | 23 ++ src/Spectre.Console/Widgets/ProgressBar.cs | 32 ++- 21 files changed, 597 insertions(+), 8 deletions(-) create mode 100644 docs/input/assets/images/barchart.png create mode 100644 docs/input/widgets/barchart.md create mode 100644 examples/Charts/Charts.csproj create mode 100644 examples/Charts/Program.cs create mode 100644 src/Spectre.Console.Tests/Expectations/BarChartTests.Should_Render_Correctly.verified.txt create mode 100644 src/Spectre.Console.Tests/Unit/BarChartTests.cs create mode 100644 src/Spectre.Console/Extensions/BarGraphExtensions.cs create mode 100644 src/Spectre.Console/Widgets/BarChart.cs create mode 100644 src/Spectre.Console/Widgets/BarChartItem.cs create mode 100644 src/Spectre.Console/Widgets/IBarGraphItem.cs diff --git a/docs/input/assets/images/barchart.png b/docs/input/assets/images/barchart.png new file mode 100644 index 0000000000000000000000000000000000000000..73e9d5553d6ea0f57b2de89ef9ae947173d93fae GIT binary patch literal 3533 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 TI6`du+QMOKXlYQP=NkDhNFB6w literal 0 HcmV?d00001 diff --git a/docs/input/widgets/barchart.md b/docs/input/widgets/barchart.md new file mode 100644 index 0000000..6e1a993 --- /dev/null +++ b/docs/input/widgets/barchart.md @@ -0,0 +1,75 @@ +Title: Bar Chart +Order: 1 +--- + +Use `BarChart` to render bar charts to the console. + + + +# Usage + +## Basic usage + +```csharp +AnsiConsole.Render(new BarChart() + .Width(60) + .Label("[green bold underline]Number of fruits[/]") + .CenterLabel() + .AddItem("Apple", 12, Color.Yellow) + .AddItem("Orange", 54, Color.Green) + .AddItem("Banana", 33, Color.Red)); +``` + +## Add items with converter + +```csharp +// Create a list of fruits +var items = new List<(string Label, double Value)> +{ + ("Apple", 12), + ("Orange", 54), + ("Banana", 33), +}; + +// Render bar chart +AnsiConsole.Render(new BarChart() + .Width(60) + .Label("[green bold underline]Number of fruits[/]") + .CenterLabel() + .AddItems(items, (item) => new BarChartItem( + item.Label, item.Value, Color.Yellow))); +``` + +## Add items implementing IBarChartItem + +```csharp +public sealed class Fruit : IBarChartItem +{ + public string Label { get; set; } + public double Value { get; set; } + public Color? Color { get; set; } + + public Fruit(string label, double value, Color? color = null) + { + Label = label; + Value = value; + Color = color; + } +} + +// Create a list of fruits +var items = new List +{ + new Fruit("Apple", 12, Color.Yellow), + new Fruit("Orange", 54, Color.Red), + new Fruit("Banana", 33, Color.Green), +}; + +// Render bar chart +AnsiConsole.Render(new BarChart() + .Width(60) + .Label("[green bold underline]Number of fruits[/]") + .CenterLabel() + .AddItem(new Fruit("Mango", 3)) + .AddItems(items)); +``` \ No newline at end of file diff --git a/docs/input/widgets/calendar.md b/docs/input/widgets/calendar.md index 3bf382c..3f3ad77 100644 --- a/docs/input/widgets/calendar.md +++ b/docs/input/widgets/calendar.md @@ -1,5 +1,5 @@ Title: Calendar -Order: 2 +Order: 3 RedirectFrom: calendar --- diff --git a/docs/input/widgets/canvas-image.md b/docs/input/widgets/canvas-image.md index ae97ce5..24fd8d4 100644 --- a/docs/input/widgets/canvas-image.md +++ b/docs/input/widgets/canvas-image.md @@ -1,5 +1,5 @@ Title: Canvas Image -Order: 5 +Order: 6 --- To add [ImageSharp](https://github.com/SixLabors/ImageSharp) superpowers to diff --git a/docs/input/widgets/canvas.md b/docs/input/widgets/canvas.md index fc8739f..847f50e 100644 --- a/docs/input/widgets/canvas.md +++ b/docs/input/widgets/canvas.md @@ -1,5 +1,5 @@ Title: Canvas -Order: 4 +Order: 5 --- `Canvas` is a widget that allows you to render arbitrary "pixels" diff --git a/docs/input/widgets/figlet.md b/docs/input/widgets/figlet.md index d6ecdeb..762ea0d 100644 --- a/docs/input/widgets/figlet.md +++ b/docs/input/widgets/figlet.md @@ -1,5 +1,5 @@ Title: Figlet -Order: 3 +Order: 4 RedirectFrom: figlet --- diff --git a/docs/input/widgets/rule.md b/docs/input/widgets/rule.md index f2020cc..ff6ac55 100644 --- a/docs/input/widgets/rule.md +++ b/docs/input/widgets/rule.md @@ -1,5 +1,5 @@ Title: Rule -Order: 1 +Order: 2 RedirectFrom: rule --- diff --git a/examples/Charts/Charts.csproj b/examples/Charts/Charts.csproj new file mode 100644 index 0000000..5b16a50 --- /dev/null +++ b/examples/Charts/Charts.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + false + Charts + Demonstrates how to render charts in a console. + + + + + + + diff --git a/examples/Charts/Program.cs b/examples/Charts/Program.cs new file mode 100644 index 0000000..08e194b --- /dev/null +++ b/examples/Charts/Program.cs @@ -0,0 +1,21 @@ +using Spectre.Console; + +namespace InfoExample +{ + public static class Program + { + public static void Main() + { + var chart = new BarChart() + .Width(60) + .Label("[green bold underline]Number of fruits[/]") + .CenterLabel() + .AddItem("Apple", 12, Color.Yellow) + .AddItem("Orange", 54, Color.Green) + .AddItem("Banana", 33, Color.Red); + + AnsiConsole.WriteLine(); + AnsiConsole.Render(chart); + } + } +} diff --git a/src/Spectre.Console.Tests/Expectations/BarChartTests.Should_Render_Correctly.verified.txt b/src/Spectre.Console.Tests/Expectations/BarChartTests.Should_Render_Correctly.verified.txt new file mode 100644 index 0000000..2ee8695 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/BarChartTests.Should_Render_Correctly.verified.txt @@ -0,0 +1,4 @@ + Number of fruits + Apple ████████ 12 +Orange █████████████████████████████████████████████████ 54 +Banana ████████████████████████████ 33 diff --git a/src/Spectre.Console.Tests/Unit/BarChartTests.cs b/src/Spectre.Console.Tests/Unit/BarChartTests.cs new file mode 100644 index 0000000..558cf52 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/BarChartTests.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using VerifyXunit; +using Xunit; + +namespace Spectre.Console.Tests.Unit +{ + [UsesVerify] + public sealed class BarChartTests + { + [Fact] + public async Task Should_Render_Correctly() + { + // Given + var console = new PlainConsole(width: 80); + + // When + console.Render(new BarChart() + .Width(60) + .Label("Number of fruits") + .AddItem("Apple", 12) + .AddItem("Orange", 54) + .AddItem("Banana", 33)); + + // Then + await Verifier.Verify(console.Output); + } + } +} diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln index 43fdbfa..fe3d4c8 100644 --- a/src/Spectre.Console.sln +++ b/src/Spectre.Console.sln @@ -62,6 +62,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Progress", "..\examples\Pro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Status", "..\examples\Status\Status.csproj", "{3716AFDF-0904-4635-8422-86E6B9356840}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Charts", "..\examples\Charts\Charts.csproj", "{0A1AFD26-86A0-4060-B277-D380172C7070}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -324,6 +326,18 @@ Global {3716AFDF-0904-4635-8422-86E6B9356840}.Release|x64.Build.0 = Release|Any CPU {3716AFDF-0904-4635-8422-86E6B9356840}.Release|x86.ActiveCfg = Release|Any CPU {3716AFDF-0904-4635-8422-86E6B9356840}.Release|x86.Build.0 = Release|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Debug|x64.ActiveCfg = Debug|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Debug|x64.Build.0 = Debug|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Debug|x86.ActiveCfg = Debug|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Debug|x86.Build.0 = Debug|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Release|Any CPU.Build.0 = Release|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Release|x64.ActiveCfg = Release|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Release|x64.Build.0 = Release|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Release|x86.ActiveCfg = Release|Any CPU + {0A1AFD26-86A0-4060-B277-D380172C7070}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -348,6 +362,7 @@ Global {5693761A-754A-40A8-9144-36510D6A4D69} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {2B712A52-40F1-4C1C-833E-7C869ACA91F3} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {3716AFDF-0904-4635-8422-86E6B9356840} = {F0575243-121F-4DEE-9F6B-246E26DC0844} + {0A1AFD26-86A0-4060-B277-D380172C7070} = {F0575243-121F-4DEE-9F6B-246E26DC0844} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} diff --git a/src/Spectre.Console/Extensions/BarGraphExtensions.cs b/src/Spectre.Console/Extensions/BarGraphExtensions.cs new file mode 100644 index 0000000..76cbff2 --- /dev/null +++ b/src/Spectre.Console/Extensions/BarGraphExtensions.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static class BarGraphExtensions + { + /// + /// Adds an item to the bar chart. + /// + /// The bar chart. + /// The item label. + /// The item value. + /// The item color. + /// The same instance so that multiple calls can be chained. + public static BarChart AddItem(this BarChart chart, string label, double value, Color? color = null) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.Data.Add(new BarChartItem(label, value, color)); + return chart; + } + + /// + /// Adds an item to the bar chart. + /// + /// A type that implements . + /// The bar chart. + /// The item. + /// The same instance so that multiple calls can be chained. + public static BarChart AddItem(this BarChart chart, T item) + where T : IBarChartItem + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.Data.Add(new BarChartItem( + item.Label, + item.Value, + item.Color)); + + return chart; + } + + /// + /// Adds multiple items to the bar chart. + /// + /// A type that implements . + /// The bar chart. + /// The items. + /// The same instance so that multiple calls can be chained. + public static BarChart AddItems(this BarChart chart, IEnumerable items) + where T : IBarChartItem + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + foreach (var item in items) + { + AddItem(chart, item); + } + + return chart; + } + + /// + /// Adds multiple items to the bar chart. + /// + /// A type that implements . + /// The bar chart. + /// The items. + /// The converter that converts instances of T to . + /// The same instance so that multiple calls can be chained. + public static BarChart AddItems(this BarChart chart, IEnumerable items, Func converter) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + if (converter is null) + { + throw new ArgumentNullException(nameof(converter)); + } + + foreach (var item in items) + { + chart.Data.Add(converter(item)); + } + + return chart; + } + + /// + /// Sets the width of the bar chart. + /// + /// The bar chart. + /// The bar chart width. + /// The same instance so that multiple calls can be chained. + public static BarChart Width(this BarChart chart, int? width) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.Width = width; + return chart; + } + + /// + /// Sets the label of the bar chart. + /// + /// The bar chart. + /// The bar chart label. + /// The same instance so that multiple calls can be chained. + public static BarChart Label(this BarChart chart, string? label) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.Label = label; + return chart; + } + + /// + /// Shows values next to each bar in the bar chart. + /// + /// The bar chart. + /// The same instance so that multiple calls can be chained. + public static BarChart ShowValues(this BarChart chart) + { + return ShowValues(chart, true); + } + + /// + /// Hides values next to each bar in the bar chart. + /// + /// The bar chart. + /// The same instance so that multiple calls can be chained. + public static BarChart HideValues(this BarChart chart) + { + return ShowValues(chart, false); + } + + /// + /// Sets whether or not values should be shown + /// next to each bar. + /// + /// The bar chart. + /// Whether or not values should be shown next to each bar. + /// The same instance so that multiple calls can be chained. + public static BarChart ShowValues(this BarChart chart, bool show) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.ShowValues = show; + return chart; + } + + /// + /// Aligns the label to the left. + /// + /// The bar chart. + /// The same instance so that multiple calls can be chained. + public static BarChart LeftAlignLabel(this BarChart chart) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.LabelAlignment = Justify.Left; + return chart; + } + + /// + /// Centers the label. + /// + /// The bar chart. + /// The same instance so that multiple calls can be chained. + public static BarChart CenterLabel(this BarChart chart) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.LabelAlignment = Justify.Center; + return chart; + } + + /// + /// Aligns the label to the right. + /// + /// The bar chart. + /// The same instance so that multiple calls can be chained. + public static BarChart RightAlignLabel(this BarChart chart) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.LabelAlignment = Justify.Right; + return chart; + } + } +} diff --git a/src/Spectre.Console/Extensions/GridExtensions.cs b/src/Spectre.Console/Extensions/GridExtensions.cs index 078edd1..be3dbe2 100644 --- a/src/Spectre.Console/Extensions/GridExtensions.cs +++ b/src/Spectre.Console/Extensions/GridExtensions.cs @@ -97,5 +97,22 @@ namespace Spectre.Console grid.AddRow(columns.Select(column => new Markup(column)).ToArray()); return grid; } + + /// + /// Sets the grid width. + /// + /// The grid. + /// The width. + /// The same instance so that multiple calls can be chained. + public static Grid Width(this Grid grid, int? width) + { + if (grid is null) + { + throw new ArgumentNullException(nameof(grid)); + } + + grid.Width = width; + return grid; + } } } diff --git a/src/Spectre.Console/Extensions/TableExtensions.cs b/src/Spectre.Console/Extensions/TableExtensions.cs index 9535b06..0ba5216 100644 --- a/src/Spectre.Console/Extensions/TableExtensions.cs +++ b/src/Spectre.Console/Extensions/TableExtensions.cs @@ -150,7 +150,7 @@ namespace Spectre.Console /// The table. /// The width. /// The same instance so that multiple calls can be chained. - public static Table Width(this Table table, int width) + public static Table Width(this Table table, int? width) { if (table is null) { diff --git a/src/Spectre.Console/Rendering/Renderable.cs b/src/Spectre.Console/Rendering/Renderable.cs index 296af6f..35f97bb 100644 --- a/src/Spectre.Console/Rendering/Renderable.cs +++ b/src/Spectre.Console/Rendering/Renderable.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics; namespace Spectre.Console.Rendering { @@ -8,12 +9,14 @@ namespace Spectre.Console.Rendering public abstract class Renderable : IRenderable { /// + [DebuggerStepThrough] Measurement IRenderable.Measure(RenderContext context, int maxWidth) { return Measure(context, maxWidth); } /// + [DebuggerStepThrough] IEnumerable IRenderable.Render(RenderContext context, int maxWidth) { return Render(context, maxWidth); diff --git a/src/Spectre.Console/Widgets/BarChart.cs b/src/Spectre.Console/Widgets/BarChart.cs new file mode 100644 index 0000000..3541311 --- /dev/null +++ b/src/Spectre.Console/Widgets/BarChart.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// A renderable (horizontal) bar chart. + /// + public sealed class BarChart : Renderable + { + /// + /// Gets the bar chart data. + /// + public List Data { get; } + + /// + /// Gets or sets the width of the bar chart. + /// + public int? Width { get; set; } + + /// + /// Gets or sets the bar chart label. + /// + public string? Label { get; set; } + + /// + /// Gets or sets the bar chart label alignment. + /// + public Justify? LabelAlignment { get; set; } = Justify.Center; + + /// + /// Gets or sets a value indicating whether or not + /// values should be shown next to each bar. + /// + public bool ShowValues { get; set; } = true; + + /// + /// Initializes a new instance of the class. + /// + public BarChart() + { + Data = new List(); + } + + /// + protected override IEnumerable Render(RenderContext context, int maxWidth) + { + var maxValue = Data.Max(item => item.Value); + + var table = new Grid(); + table.Collapse(); + table.AddColumn(new GridColumn().PadRight(2).RightAligned()); + table.AddColumn(new GridColumn().PadLeft(0)); + table.Width = Width; + + if (!string.IsNullOrWhiteSpace(Label)) + { + table.AddRow(Text.Empty, new Markup(Label).Alignment(LabelAlignment)); + } + + foreach (var item in Data) + { + table.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, + }); + } + + return ((IRenderable)table).Render(context, maxWidth); + } + } +} diff --git a/src/Spectre.Console/Widgets/BarChartItem.cs b/src/Spectre.Console/Widgets/BarChartItem.cs new file mode 100644 index 0000000..094f036 --- /dev/null +++ b/src/Spectre.Console/Widgets/BarChartItem.cs @@ -0,0 +1,38 @@ +using System; + +namespace Spectre.Console +{ + /// + /// An item that's shown in a bar chart. + /// + public sealed class BarChartItem : IBarChartItem + { + /// + /// Gets the item label. + /// + public string Label { get; } + + /// + /// Gets the item value. + /// + public double Value { get; } + + /// + /// Gets the item color. + /// + public Color? Color { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The item label. + /// The item value. + /// The item color. + public BarChartItem(string label, double value, Color? color = null) + { + Label = label ?? throw new ArgumentNullException(nameof(label)); + Value = value; + Color = color; + } + } +} diff --git a/src/Spectre.Console/Widgets/Grid.cs b/src/Spectre.Console/Widgets/Grid.cs index 9fccaf9..7fedde1 100644 --- a/src/Spectre.Console/Widgets/Grid.cs +++ b/src/Spectre.Console/Widgets/Grid.cs @@ -42,6 +42,11 @@ namespace Spectre.Console set => MarkAsDirty(() => _alignment = value); } + /// + /// Gets or sets the width of the grid. + /// + public int? Width { get; set; } + /// /// Initializes a new instance of the class. /// @@ -124,6 +129,7 @@ namespace Spectre.Console ShowHeaders = false, IsGrid = true, PadRightCell = _padRightCell, + Width = Width, }; foreach (var column in _columns) diff --git a/src/Spectre.Console/Widgets/IBarGraphItem.cs b/src/Spectre.Console/Widgets/IBarGraphItem.cs new file mode 100644 index 0000000..3f8187c --- /dev/null +++ b/src/Spectre.Console/Widgets/IBarGraphItem.cs @@ -0,0 +1,23 @@ +namespace Spectre.Console +{ + /// + /// Represents a bar chart item. + /// + public interface IBarChartItem + { + /// + /// Gets the item label. + /// + string Label { get; } + + /// + /// Gets the item value. + /// + double Value { get; } + + /// + /// Gets the item color. + /// + Color? Color { get; } + } +} diff --git a/src/Spectre.Console/Widgets/ProgressBar.cs b/src/Spectre.Console/Widgets/ProgressBar.cs index 3c4e742..9fe16f4 100644 --- a/src/Spectre.Console/Widgets/ProgressBar.cs +++ b/src/Spectre.Console/Widgets/ProgressBar.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using Spectre.Console.Rendering; namespace Spectre.Console @@ -10,6 +11,10 @@ namespace Spectre.Console public double MaxValue { get; set; } = 100; public int? Width { get; set; } + public bool ShowRemaining { get; set; } = true; + public char UnicodeBar { get; set; } = '━'; + public char AsciiBar { get; set; } = '-'; + public bool ShowValue { get; set; } public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow); public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green); @@ -26,15 +31,38 @@ namespace Spectre.Console var width = Math.Min(Width ?? maxWidth, maxWidth); var completed = Math.Min(MaxValue, Math.Max(0, Value)); - var token = !context.Unicode || context.LegacyConsole ? '-' : '━'; + var token = !context.Unicode || context.LegacyConsole ? AsciiBar : UnicodeBar; var style = completed >= MaxValue ? FinishedStyle : CompletedStyle; var bars = Math.Max(0, (int)(width * (completed / MaxValue))); + + var value = completed.ToString(CultureInfo.InvariantCulture); + if (ShowValue) + { + bars = bars - value.Length - 1; + } + yield return new Segment(new string(token, bars), style); + if (ShowValue) + { + yield return new Segment(" " + value, style); + } + if (bars < width) { - yield return new Segment(new string(token, width - bars), RemainingStyle); + var diff = width - bars; + if (ShowValue) + { + diff = diff - value.Length - 1; + if (diff <= 0) + { + yield break; + } + } + + var remainingToken = ShowRemaining ? token : ' '; + yield return new Segment(new string(remainingToken, diff), RemainingStyle); } } }