From b64e016e8c744fa60934ce5808642c88c1c05183 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Sun, 31 Jan 2021 22:46:15 +0100 Subject: [PATCH] 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 --- examples/Console/Charts/Program.cs | 29 +- .../Borders/Box/NoBorder.Output.verified.txt | 4 +- .../BarChart/Zero_Value.Output.verified.txt | 4 + .../BreakdownChart/Ansi.Output.verified.txt | 4 + .../Culture.Output.verified.txt | 3 + .../Default.Output.verified.txt | 2 + .../FullSize.Output.verified.txt | 4 + .../HideTagValues.Output.verified.txt | 2 + .../HideTags.Output.verified.txt | 1 + .../ShowAsPercentages.Output.verified.txt | 3 + .../BreakdownChart/Width.Output.verified.txt | 3 + .../Spectre.Console.Tests.csproj | 2 +- .../Unit/BarChartTests.cs | 19 ++ .../Unit/BreakdownChartTests.cs | 186 ++++++++++++ ...aphExtensions.cs => BarChartExtensions.cs} | 18 +- .../Extensions/BreakdownChartExtensions.cs | 266 ++++++++++++++++++ .../Extensions/DayOfWeekExtensions.cs | 2 +- .../Extensions/StringExtensions.cs | 2 +- src/Spectre.Console/IHasCulture.cs | 2 +- src/Spectre.Console/Internal/Ratio.cs | 2 +- src/Spectre.Console/Widgets/Calendar.cs | 4 +- .../Widgets/{ => Charts}/BarChart.cs | 27 +- .../Widgets/{ => Charts}/BarChartItem.cs | 0 .../Widgets/Charts/BreakdownBar.cs | 42 +++ .../Widgets/Charts/BreakdownChart.cs | 102 +++++++ .../Widgets/Charts/BreakdownChartItem.cs | 38 +++ .../Widgets/Charts/BreakdownTags.cs | 69 +++++ .../IBarChartItem.cs} | 0 .../Widgets/Charts/IBreakdownChartItem.cs | 23 ++ src/Spectre.Console/Widgets/Panel.cs | 71 ++++- src/Spectre.Console/Widgets/ProgressBar.cs | 16 +- .../Widgets/Table/TableRenderer.cs | 2 +- 32 files changed, 911 insertions(+), 41 deletions(-) create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/BarChart/Zero_Value.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Ansi.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Culture.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Default.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/FullSize.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/HideTagValues.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/HideTags.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/ShowAsPercentages.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Width.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Unit/BreakdownChartTests.cs rename src/Spectre.Console/Extensions/{BarGraphExtensions.cs => BarChartExtensions.cs} (95%) create mode 100644 src/Spectre.Console/Extensions/BreakdownChartExtensions.cs rename src/Spectre.Console/Widgets/{ => Charts}/BarChart.cs (73%) rename src/Spectre.Console/Widgets/{ => Charts}/BarChartItem.cs (100%) create mode 100644 src/Spectre.Console/Widgets/Charts/BreakdownBar.cs create mode 100644 src/Spectre.Console/Widgets/Charts/BreakdownChart.cs create mode 100644 src/Spectre.Console/Widgets/Charts/BreakdownChartItem.cs create mode 100644 src/Spectre.Console/Widgets/Charts/BreakdownTags.cs rename src/Spectre.Console/Widgets/{IBarGraphItem.cs => Charts/IBarChartItem.cs} (100%) create mode 100644 src/Spectre.Console/Widgets/Charts/IBreakdownChartItem.cs diff --git a/examples/Console/Charts/Program.cs b/examples/Console/Charts/Program.cs index 08e194b..b4fdb4b 100644 --- a/examples/Console/Charts/Program.cs +++ b/examples/Console/Charts/Program.cs @@ -1,21 +1,42 @@ using Spectre.Console; +using Spectre.Console.Rendering; -namespace InfoExample +namespace Charts { public static class Program { public static void Main() { - var chart = new BarChart() + // Render a bar chart + AnsiConsole.WriteLine(); + Render("Fruits per month", 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); + .AddItem("Banana", 33, Color.Red)); + // Render a breakdown chart AnsiConsole.WriteLine(); - AnsiConsole.Render(chart); + Render("Languages", new BreakdownChart() + .FullSize() + .Width(60) + .ShowAsPercentages() + .AddItem("SCSS", 37, Color.Red) + .AddItem("HTML", 28.3, Color.Blue) + .AddItem("C#", 22.6, Color.Green) + .AddItem("JavaScript", 6, Color.Yellow) + .AddItem("Ruby", 6, Color.LightGreen) + .AddItem("Shell", 0.1, Color.Aqua)); + } + + private static void Render(string title, IRenderable chart) + { + AnsiConsole.Render( + new Panel(chart) + .Padding(1, 1) + .Header(title)); } } } diff --git a/src/Spectre.Console.Tests/Expectations/Borders/Box/NoBorder.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Borders/Box/NoBorder.Output.verified.txt index 684af3c..0d3e931 100644 --- a/src/Spectre.Console.Tests/Expectations/Borders/Box/NoBorder.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Borders/Box/NoBorder.Output.verified.txt @@ -1,3 +1 @@ - Greeting - Hello World - + Hello World diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/BarChart/Zero_Value.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/BarChart/Zero_Value.Output.verified.txt new file mode 100644 index 0000000..d869315 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/BarChart/Zero_Value.Output.verified.txt @@ -0,0 +1,4 @@ + Number of fruits + Apple 0 +Orange █████████████████████████████████████████████████ 54 +Banana ████████████████████████████ 33 diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Ansi.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Ansi.Output.verified.txt new file mode 100644 index 0000000..37c309b --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Ansi.Output.verified.txt @@ -0,0 +1,4 @@ +████████████████████████████████████████████████████████████ + +■ SCSS 37 ■ HTML 28.3 ■ C# 22.6 ■ JavaScript 6 +■ Ruby 6 ■ Shell 0.1 diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Culture.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Culture.Output.verified.txt new file mode 100644 index 0000000..ed94ed7 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Culture.Output.verified.txt @@ -0,0 +1,3 @@ +████████████████████████████████████████████████████████████ +■ SCSS 37 ■ HTML 28,3 ■ C# 22,6 ■ JavaScript 6 +■ Ruby 6 ■ Shell 0,1 diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Default.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Default.Output.verified.txt new file mode 100644 index 0000000..f67f8d1 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Default.Output.verified.txt @@ -0,0 +1,2 @@ +████████████████████████████████████████████████████████████████████████████████ +■ SCSS 37 ■ HTML 28.3 ■ C# 22.6 ■ JavaScript 6 ■ Ruby 6 ■ Shell 0.1 diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/FullSize.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/FullSize.Output.verified.txt new file mode 100644 index 0000000..2840336 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/FullSize.Output.verified.txt @@ -0,0 +1,4 @@ +████████████████████████████████████████████████████████████ + +■ SCSS 37 ■ HTML 28.3 ■ C# 22.6 ■ JavaScript 6 +■ Ruby 6 ■ Shell 0.1 diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/HideTagValues.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/HideTagValues.Output.verified.txt new file mode 100644 index 0000000..eee961f --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/HideTagValues.Output.verified.txt @@ -0,0 +1,2 @@ +████████████████████████████████████████████████████████████ +■ SCSS ■ HTML ■ C# ■ JavaScript ■ Ruby ■ Shell diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/HideTags.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/HideTags.Output.verified.txt new file mode 100644 index 0000000..b92f389 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/HideTags.Output.verified.txt @@ -0,0 +1 @@ +████████████████████████████████████████████████████████████ diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/ShowAsPercentages.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/ShowAsPercentages.Output.verified.txt new file mode 100644 index 0000000..4e59f8a --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/ShowAsPercentages.Output.verified.txt @@ -0,0 +1,3 @@ +████████████████████████████████████████████████████████████ +■ SCSS 37% ■ HTML 28.3% ■ C# 22.6% ■ JavaScript 6% +■ Ruby 6% ■ Shell 0.1% diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Width.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Width.Output.verified.txt new file mode 100644 index 0000000..5d53b03 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/BreakdownChart/Width.Output.verified.txt @@ -0,0 +1,3 @@ +████████████████████████████████████████████████████████████ +■ SCSS 37 ■ HTML 28.3 ■ C# 22.6 ■ JavaScript 6 +■ Ruby 6 ■ Shell 0.1 diff --git a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj index 34cda0a..f667424 100644 --- a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj +++ b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Spectre.Console.Tests/Unit/BarChartTests.cs b/src/Spectre.Console.Tests/Unit/BarChartTests.cs index 4455998..988f96a 100644 --- a/src/Spectre.Console.Tests/Unit/BarChartTests.cs +++ b/src/Spectre.Console.Tests/Unit/BarChartTests.cs @@ -28,5 +28,24 @@ namespace Spectre.Console.Tests.Unit // Then await Verifier.Verify(console.Output); } + + [Fact] + [Expectation("Zero_Value")] + public async Task Should_Render_Correctly_2() + { + // Given + var console = new FakeConsole(width: 80); + + // When + console.Render(new BarChart() + .Width(60) + .Label("Number of fruits") + .AddItem("Apple", 0) + .AddItem("Orange", 54) + .AddItem("Banana", 33)); + + // Then + await Verifier.Verify(console.Output); + } } } diff --git a/src/Spectre.Console.Tests/Unit/BreakdownChartTests.cs b/src/Spectre.Console.Tests/Unit/BreakdownChartTests.cs new file mode 100644 index 0000000..491e304 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/BreakdownChartTests.cs @@ -0,0 +1,186 @@ +using System.Threading.Tasks; +using Spectre.Console.Testing; +using Spectre.Verify.Extensions; +using VerifyXunit; +using Xunit; + +namespace Spectre.Console.Tests.Unit +{ + [UsesVerify] + [ExpectationPath("Widgets/BreakdownChart")] + public sealed class BreakdownChartTests + { + [Fact] + [Expectation("Default")] + public async Task Should_Render_Correctly() + { + // Given + var console = new FakeConsole(width: 80); + + // When + console.Render(new BreakdownChart() + .AddItem("SCSS", 37, Color.Red) + .AddItem("HTML", 28.3, Color.Blue) + .AddItem("C#", 22.6, Color.Green) + .AddItem("JavaScript", 6, Color.Yellow) + .AddItem("Ruby", 6, Color.LightGreen) + .AddItem("Shell", 0.1, Color.Aqua)); + + // Then + await Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Width")] + public async Task Should_Render_With_Specific_Width() + { + // Given + var console = new FakeConsole(width: 80); + + // When + console.Render(new BreakdownChart() + .Width(60) + .AddItem("SCSS", 37, Color.Red) + .AddItem("HTML", 28.3, Color.Blue) + .AddItem("C#", 22.6, Color.Green) + .AddItem("JavaScript", 6, Color.Yellow) + .AddItem("Ruby", 6, Color.LightGreen) + .AddItem("Shell", 0.1, Color.Aqua)); + + // Then + await Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("ShowAsPercentages")] + public async Task Should_Render_Correctly_With_Specific_Width() + { + // Given + var console = new FakeConsole(width: 80); + + // When + console.Render(new BreakdownChart() + .Width(60) + .ShowAsPercentages() + .AddItem("SCSS", 37, Color.Red) + .AddItem("HTML", 28.3, Color.Blue) + .AddItem("C#", 22.6, Color.Green) + .AddItem("JavaScript", 6, Color.Yellow) + .AddItem("Ruby", 6, Color.LightGreen) + .AddItem("Shell", 0.1, Color.Aqua)); + + // Then + await Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("HideTags")] + public async Task Should_Render_Correctly_Without_Tags() + { + // Given + var console = new FakeConsole(width: 80); + + // When + console.Render(new BreakdownChart() + .Width(60) + .HideTags() + .AddItem("SCSS", 37, Color.Red) + .AddItem("HTML", 28.3, Color.Blue) + .AddItem("C#", 22.6, Color.Green) + .AddItem("JavaScript", 6, Color.Yellow) + .AddItem("Ruby", 6, Color.LightGreen) + .AddItem("Shell", 0.1, Color.Aqua)); + + // Then + await Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("HideTagValues")] + public async Task Should_Render_Correctly_Without_Tag_Values() + { + // Given + var console = new FakeConsole(width: 80); + + // When + console.Render(new BreakdownChart() + .Width(60) + .HideTagValues() + .AddItem("SCSS", 37, Color.Red) + .AddItem("HTML", 28.3, Color.Blue) + .AddItem("C#", 22.6, Color.Green) + .AddItem("JavaScript", 6, Color.Yellow) + .AddItem("Ruby", 6, Color.LightGreen) + .AddItem("Shell", 0.1, Color.Aqua)); + + // Then + await Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Culture")] + public async Task Should_Render_Correctly_With_Specific_Culture() + { + // Given + var console = new FakeConsole(width: 80); + + // When + console.Render(new BreakdownChart() + .Width(60) + .Culture("sv-SE") + .AddItem("SCSS", 37, Color.Red) + .AddItem("HTML", 28.3, Color.Blue) + .AddItem("C#", 22.6, Color.Green) + .AddItem("JavaScript", 6, Color.Yellow) + .AddItem("Ruby", 6, Color.LightGreen) + .AddItem("Shell", 0.1, Color.Aqua)); + + // Then + await Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("FullSize")] + public async Task Should_Render_FullSize_Mode_Correctly() + { + // Given + var console = new FakeConsole(width: 80); + + // When + console.Render(new BreakdownChart() + .Width(60) + .FullSize() + .AddItem("SCSS", 37, Color.Red) + .AddItem("HTML", 28.3, Color.Blue) + .AddItem("C#", 22.6, Color.Green) + .AddItem("JavaScript", 6, Color.Yellow) + .AddItem("Ruby", 6, Color.LightGreen) + .AddItem("Shell", 0.1, Color.Aqua)); + + // Then + await Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Ansi")] + public async Task Should_Render_Correct_Ansi() + { + // Given + var console = new FakeAnsiConsole(ColorSystem.EightBit, width: 80); + + // When + console.Render(new BreakdownChart() + .Width(60) + .FullSize() + .AddItem("SCSS", 37, Color.Red) + .AddItem("HTML", 28.3, Color.Blue) + .AddItem("C#", 22.6, Color.Green) + .AddItem("JavaScript", 6, Color.Yellow) + .AddItem("Ruby", 6, Color.LightGreen) + .AddItem("Shell", 0.1, Color.Aqua)); + + // Then + await Verifier.Verify(console.Output); + } + } +} diff --git a/src/Spectre.Console/Extensions/BarGraphExtensions.cs b/src/Spectre.Console/Extensions/BarChartExtensions.cs similarity index 95% rename from src/Spectre.Console/Extensions/BarGraphExtensions.cs rename to src/Spectre.Console/Extensions/BarChartExtensions.cs index 97f5091..d54f608 100644 --- a/src/Spectre.Console/Extensions/BarGraphExtensions.cs +++ b/src/Spectre.Console/Extensions/BarChartExtensions.cs @@ -6,7 +6,7 @@ namespace Spectre.Console /// /// Contains extension methods for . /// - public static class BarGraphExtensions + public static class BarChartExtensions { /// /// Adds an item to the bar chart. @@ -42,10 +42,18 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(chart)); } - chart.Data.Add(new BarChartItem( - item.Label, - item.Value, - item.Color)); + if (item is BarChartItem barChartItem) + { + chart.Data.Add(barChartItem); + } + else + { + chart.Data.Add( + new BarChartItem( + item.Label, + item.Value, + item.Color)); + } return chart; } diff --git a/src/Spectre.Console/Extensions/BreakdownChartExtensions.cs b/src/Spectre.Console/Extensions/BreakdownChartExtensions.cs new file mode 100644 index 0000000..8d4cceb --- /dev/null +++ b/src/Spectre.Console/Extensions/BreakdownChartExtensions.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static class BreakdownChartExtensions + { + /// + /// Adds an item to the breakdown chart. + /// + /// The breakdown chart. + /// The item label. + /// The item value. + /// The item color. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart AddItem(this BreakdownChart chart, string label, double value, Color color) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.Data.Add(new BreakdownChartItem(label, value, color)); + return chart; + } + + /// + /// Adds an item to the breakdown chart. + /// + /// A type that implements . + /// The breakdown chart. + /// The item. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart AddItem(this BreakdownChart chart, T item) + where T : IBreakdownChartItem + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + if (item is BreakdownChartItem chartItem) + { + chart.Data.Add(chartItem); + } + else + { + chart.Data.Add( + new BreakdownChartItem( + item.Label, + item.Value, + item.Color)); + } + + return chart; + } + + /// + /// Adds multiple items to the breakdown chart. + /// + /// A type that implements . + /// The breakdown chart. + /// The items. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart AddItems(this BreakdownChart chart, IEnumerable items) + where T : IBreakdownChartItem + { + 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 breakdown chart. + /// + /// A type that implements . + /// The breakdown chart. + /// The items. + /// The converter that converts instances of T to . + /// The same instance so that multiple calls can be chained. + public static BreakdownChart AddItems(this BreakdownChart 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 breakdown chart. + /// + /// The breakdown chart. + /// The breakdown chart width. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart Width(this BreakdownChart chart, int? width) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.Width = width; + return chart; + } + + /// + /// All values will be shown as percentages. + /// + /// The breakdown chart. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart ShowAsPercentages(this BreakdownChart chart) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.ShowAsPercentages = true; + return chart; + } + + /// + /// Tags will be shown. + /// + /// The breakdown chart. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart ShowTags(this BreakdownChart chart) + { + return ShowTags(chart, true); + } + + /// + /// Tags will be not be shown. + /// + /// The breakdown chart. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart HideTags(this BreakdownChart chart) + { + return ShowTags(chart, false); + } + + /// + /// Sets whether or not tags will be shown. + /// + /// The breakdown chart. + /// Whether or not tags will be shown. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart ShowTags(this BreakdownChart chart, bool show) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.ShowTags = show; + return chart; + } + + /// + /// Tag values will be shown. + /// + /// The breakdown chart. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart ShowTagValues(this BreakdownChart chart) + { + return ShowTagValues(chart, true); + } + + /// + /// Tag values will be not be shown. + /// + /// The breakdown chart. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart HideTagValues(this BreakdownChart chart) + { + return ShowTagValues(chart, false); + } + + /// + /// Sets whether or not tag values will be shown. + /// + /// The breakdown chart. + /// Whether or not tag values will be shown. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart ShowTagValues(this BreakdownChart chart, bool show) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.ShowTagValues = show; + return chart; + } + + /// + /// Chart and tags is rendered in compact mode. + /// + /// The breakdown chart. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart Compact(this BreakdownChart chart) + { + return Compact(chart, true); + } + + /// + /// Chart and tags is rendered in full size mode. + /// + /// The breakdown chart. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart FullSize(this BreakdownChart chart) + { + return Compact(chart, false); + } + + /// + /// Sets whether or not the chart and tags should be rendered in compact mode. + /// + /// The breakdown chart. + /// Whether or not the chart and tags should be rendered in compact mode. + /// The same instance so that multiple calls can be chained. + public static BreakdownChart Compact(this BreakdownChart chart, bool compact) + { + if (chart is null) + { + throw new ArgumentNullException(nameof(chart)); + } + + chart.Compact = compact; + return chart; + } + } +} diff --git a/src/Spectre.Console/Extensions/DayOfWeekExtensions.cs b/src/Spectre.Console/Extensions/DayOfWeekExtensions.cs index 8b597c3..557d81d 100644 --- a/src/Spectre.Console/Extensions/DayOfWeekExtensions.cs +++ b/src/Spectre.Console/Extensions/DayOfWeekExtensions.cs @@ -10,7 +10,7 @@ namespace Spectre.Console culture ??= CultureInfo.InvariantCulture; return culture.DateTimeFormat .GetAbbreviatedDayName(day) - .Capitalize(culture); + .CapitalizeFirstLetter(culture); } public static DayOfWeek GetNextWeekDay(this DayOfWeek day) diff --git a/src/Spectre.Console/Extensions/StringExtensions.cs b/src/Spectre.Console/Extensions/StringExtensions.cs index f232dce..f5e150a 100644 --- a/src/Spectre.Console/Extensions/StringExtensions.cs +++ b/src/Spectre.Console/Extensions/StringExtensions.cs @@ -44,7 +44,7 @@ namespace Spectre.Console return Cell.GetCellLength(context, text); } - internal static string Capitalize(this string? text, CultureInfo? culture = null) + internal static string CapitalizeFirstLetter(this string? text, CultureInfo? culture = null) { if (text == null) { diff --git a/src/Spectre.Console/IHasCulture.cs b/src/Spectre.Console/IHasCulture.cs index f1a1073..f850e38 100644 --- a/src/Spectre.Console/IHasCulture.cs +++ b/src/Spectre.Console/IHasCulture.cs @@ -10,6 +10,6 @@ namespace Spectre.Console /// /// Gets or sets the culture. /// - CultureInfo Culture { get; set; } + CultureInfo? Culture { get; set; } } } diff --git a/src/Spectre.Console/Internal/Ratio.cs b/src/Spectre.Console/Internal/Ratio.cs index d8fb1ca..163ce08 100644 --- a/src/Spectre.Console/Internal/Ratio.cs +++ b/src/Spectre.Console/Internal/Ratio.cs @@ -40,7 +40,7 @@ namespace Spectre.Console return result; } - public static List Distribute(int total, List ratios, List? minimums = null) + public static List Distribute(int total, IList ratios, IList? minimums = null) { if (minimums != null) { diff --git a/src/Spectre.Console/Widgets/Calendar.cs b/src/Spectre.Console/Widgets/Calendar.cs index f7bea90..24a4bae 100644 --- a/src/Spectre.Console/Widgets/Calendar.cs +++ b/src/Spectre.Console/Widgets/Calendar.cs @@ -22,7 +22,7 @@ namespace Spectre.Console private TableBorder _border; private bool _useSafeBorder; private Style? _borderStyle; - private CultureInfo _culture; + private CultureInfo? _culture; private Style _highlightStyle; private bool _showHeader; private Style? _headerStyle; @@ -79,7 +79,7 @@ namespace Spectre.Console /// /// Gets or sets the calendar's . /// - public CultureInfo Culture + public CultureInfo? Culture { get => _culture; set => MarkAsDirty(() => _culture = value); diff --git a/src/Spectre.Console/Widgets/BarChart.cs b/src/Spectre.Console/Widgets/Charts/BarChart.cs similarity index 73% rename from src/Spectre.Console/Widgets/BarChart.cs rename to src/Spectre.Console/Widgets/Charts/BarChart.cs index 098f863..169f4a0 100644 --- a/src/Spectre.Console/Widgets/BarChart.cs +++ b/src/Spectre.Console/Widgets/Charts/BarChart.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Spectre.Console.Rendering; @@ -7,12 +9,12 @@ namespace Spectre.Console /// /// A renderable (horizontal) bar chart. /// - public sealed class BarChart : Renderable + public sealed class BarChart : Renderable, IHasCulture { /// /// Gets the bar chart data. /// - public List Data { get; } + public List Data { get; } /// /// Gets or sets the width of the bar chart. @@ -35,24 +37,38 @@ namespace Spectre.Console /// public bool ShowValues { get; set; } = true; + /// + /// Gets or sets the culture that's used to format values. + /// + /// Defaults to invariant culture. + public CultureInfo? Culture { get; set; } + /// /// Initializes a new instance of the class. /// public BarChart() { - Data = new List(); + Data = new List(); + } + + /// + protected override Measurement Measure(RenderContext context, int maxWidth) + { + var width = Math.Min(Width ?? maxWidth, maxWidth); + return new Measurement(width, width); } /// protected override IEnumerable 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; + grid.Width = width; if (!string.IsNullOrWhiteSpace(Label)) { @@ -73,10 +89,11 @@ namespace Spectre.Console UnicodeBar = '█', AsciiBar = '█', ShowValue = ShowValues, + Culture = Culture, }); } - return ((IRenderable)grid).Render(context, maxWidth); + return ((IRenderable)grid).Render(context, width); } } } diff --git a/src/Spectre.Console/Widgets/BarChartItem.cs b/src/Spectre.Console/Widgets/Charts/BarChartItem.cs similarity index 100% rename from src/Spectre.Console/Widgets/BarChartItem.cs rename to src/Spectre.Console/Widgets/Charts/BarChartItem.cs diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs b/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs new file mode 100644 index 0000000..8bdf61e --- /dev/null +++ b/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs @@ -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 _data; + + public int? Width { get; set; } + + public BreakdownBar(List 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 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; + } + } +} diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs b/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs new file mode 100644 index 0000000..249ccae --- /dev/null +++ b/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// A renderable breakdown chart. + /// + public sealed class BreakdownChart : Renderable, IHasCulture + { + /// + /// Gets the breakdown chart data. + /// + public List Data { get; } + + /// + /// Gets or sets the width of the breakdown chart. + /// + public int? Width { get; set; } + + /// + /// Gets or sets a value indicating whether or not + /// to show values as percentages or not. + /// + public bool ShowAsPercentages { get; set; } + + /// + /// Gets or sets a value indicating whether or not to show tags. + /// + public bool ShowTags { get; set; } = true; + + /// + /// Gets or sets a value indicating whether or not to show tag values. + /// + public bool ShowTagValues { get; set; } = true; + + /// + /// Gets or sets a value indicating whether or not the + /// chart and tags should be rendered in compact mode. + /// + public bool Compact { get; set; } = true; + + /// + /// Gets or sets the to use + /// when rendering values. + /// + /// Defaults to invariant culture. + public CultureInfo? Culture { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public BreakdownChart() + { + Data = new List(); + Culture = CultureInfo.InvariantCulture; + } + + /// + protected override Measurement Measure(RenderContext context, int maxWidth) + { + var width = Math.Min(Width ?? maxWidth, maxWidth); + return new Measurement(width, width); + } + + /// + protected override IEnumerable 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); + } + } +} diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownChartItem.cs b/src/Spectre.Console/Widgets/Charts/BreakdownChartItem.cs new file mode 100644 index 0000000..5a0529d --- /dev/null +++ b/src/Spectre.Console/Widgets/Charts/BreakdownChartItem.cs @@ -0,0 +1,38 @@ +using System; + +namespace Spectre.Console +{ + /// + /// An item that's shown in a breakdown chart. + /// + public sealed class BreakdownChartItem : IBreakdownChartItem + { + /// + /// 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 BreakdownChartItem(string label, double value, Color color) + { + Label = label ?? throw new ArgumentNullException(nameof(label)); + Value = value; + Color = color; + } + } +} diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs b/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs new file mode 100644 index 0000000..71e8d92 --- /dev/null +++ b/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs @@ -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 _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 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 Render(RenderContext context, int maxWidth) + { + var culture = Culture ?? CultureInfo.InvariantCulture; + + var panels = new List(); + 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(); + } + } +} diff --git a/src/Spectre.Console/Widgets/IBarGraphItem.cs b/src/Spectre.Console/Widgets/Charts/IBarChartItem.cs similarity index 100% rename from src/Spectre.Console/Widgets/IBarGraphItem.cs rename to src/Spectre.Console/Widgets/Charts/IBarChartItem.cs diff --git a/src/Spectre.Console/Widgets/Charts/IBreakdownChartItem.cs b/src/Spectre.Console/Widgets/Charts/IBreakdownChartItem.cs new file mode 100644 index 0000000..1568756 --- /dev/null +++ b/src/Spectre.Console/Widgets/Charts/IBreakdownChartItem.cs @@ -0,0 +1,23 @@ +namespace Spectre.Console +{ + /// + /// Represents a breakdown chart item. + /// + public interface IBreakdownChartItem + { + /// + /// 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/Panel.cs b/src/Spectre.Console/Widgets/Panel.cs index d737592..d490bf7 100644 --- a/src/Spectre.Console/Widgets/Panel.cs +++ b/src/Spectre.Console/Widgets/Panel.cs @@ -40,6 +40,11 @@ namespace Spectre.Console /// public PanelHeader? Header { get; set; } + /// + /// Gets or sets a value indicating whether or not the panel is inlined. + /// + internal bool Inline { get; set; } + /// /// Initializes a new instance of the class. /// @@ -71,29 +76,41 @@ namespace Spectre.Console /// protected override IEnumerable Render(RenderContext context, int maxWidth) { + var edgeWidth = EdgeWidth; + var border = BoxExtensions.GetSafeBorder(Border, (context.LegacyConsole || !context.Unicode) && UseSafeBorder); var borderStyle = BorderStyle ?? Style.Plain; + var showBorder = true; + if (border is NoBoxBorder) + { + showBorder = false; + edgeWidth = 0; + } + var child = new Padder(_child, Padding); - var childWidth = maxWidth - EdgeWidth; + var childWidth = maxWidth - edgeWidth; if (!Expand) { - var measurement = ((IRenderable)child).Measure(context, maxWidth - EdgeWidth); + var measurement = ((IRenderable)child).Measure(context, maxWidth - edgeWidth); childWidth = measurement.Max; } - var panelWidth = childWidth + EdgeWidth; + var panelWidth = childWidth + edgeWidth; panelWidth = Math.Min(panelWidth, maxWidth); var result = new List(); - // Panel top - AddTopBorder(result, context, border, borderStyle, panelWidth); + if (showBorder) + { + // Panel top + AddTopBorder(result, context, border, borderStyle, panelWidth); + } // Split the child segments into lines. var childSegments = ((IRenderable)child).Render(context, childWidth); - foreach (var line in Segment.SplitLines(context, childSegments, childWidth)) + foreach (var (_, _, last, line) in Segment.SplitLines(context, childSegments, childWidth).Enumerate()) { if (line.Count == 1 && line[0].IsWhiteSpace) { @@ -102,7 +119,10 @@ namespace Spectre.Console continue; } - result.Add(new Segment(border.GetPart(BoxBorderPart.Left), borderStyle)); + if (showBorder) + { + result.Add(new Segment(border.GetPart(BoxBorderPart.Left), borderStyle)); + } var content = new List(); content.AddRange(line); @@ -117,20 +137,45 @@ namespace Spectre.Console result.AddRange(content); - result.Add(new Segment(border.GetPart(BoxBorderPart.Right), borderStyle)); + if (showBorder) + { + result.Add(new Segment(border.GetPart(BoxBorderPart.Right), borderStyle)); + } + + // Don't emit a line break if this is the last + // line, we're not showing the border, and we're + // not rendering this inline. + var emitLinebreak = !(last && !showBorder && !Inline); + if (!emitLinebreak) + { + continue; + } + result.Add(Segment.LineBreak); } // Panel bottom - result.Add(new Segment(border.GetPart(BoxBorderPart.BottomLeft), borderStyle)); - result.Add(new Segment(border.GetPart(BoxBorderPart.Bottom).Repeat(panelWidth - EdgeWidth), borderStyle)); - result.Add(new Segment(border.GetPart(BoxBorderPart.BottomRight), borderStyle)); - result.Add(Segment.LineBreak); + if (showBorder) + { + result.Add(new Segment(border.GetPart(BoxBorderPart.BottomLeft), borderStyle)); + result.Add(new Segment(border.GetPart(BoxBorderPart.Bottom).Repeat(panelWidth - EdgeWidth), borderStyle)); + result.Add(new Segment(border.GetPart(BoxBorderPart.BottomRight), borderStyle)); + } + + // TODO: Need a better name for this? + // If we're rendering this as part of an inline parent renderable, + // such as columns, we should not emit the last line break. + if (!Inline) + { + result.Add(Segment.LineBreak); + } return result; } - private void AddTopBorder(List result, RenderContext context, BoxBorder border, Style borderStyle, int panelWidth) + private void AddTopBorder( + List result, RenderContext context, BoxBorder border, + Style borderStyle, int panelWidth) { var rule = new Rule { diff --git a/src/Spectre.Console/Widgets/ProgressBar.cs b/src/Spectre.Console/Widgets/ProgressBar.cs index 9fe16f4..f54cb2a 100644 --- a/src/Spectre.Console/Widgets/ProgressBar.cs +++ b/src/Spectre.Console/Widgets/ProgressBar.cs @@ -5,7 +5,7 @@ using Spectre.Console.Rendering; namespace Spectre.Console { - internal sealed class ProgressBar : Renderable + internal sealed class ProgressBar : Renderable, IHasCulture { public double Value { get; set; } public double MaxValue { get; set; } = 100; @@ -15,6 +15,7 @@ namespace Spectre.Console public char UnicodeBar { get; set; } = '━'; public char AsciiBar { get; set; } = '-'; public bool ShowValue { get; set; } + public CultureInfo? Culture { get; set; } public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow); public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green); @@ -36,17 +37,26 @@ namespace Spectre.Console var bars = Math.Max(0, (int)(width * (completed / MaxValue))); - var value = completed.ToString(CultureInfo.InvariantCulture); + var value = completed.ToString(Culture ?? CultureInfo.InvariantCulture); if (ShowValue) { bars = bars - value.Length - 1; + bars = Math.Max(0, bars); } yield return new Segment(new string(token, bars), style); if (ShowValue) { - yield return new Segment(" " + value, style); + // TODO: Fix this at some point + if (bars == 0) + { + yield return new Segment(value, style); + } + else + { + yield return new Segment(" " + value, style); + } } if (bars < width) diff --git a/src/Spectre.Console/Widgets/Table/TableRenderer.cs b/src/Spectre.Console/Widgets/Table/TableRenderer.cs index c66b663..32d8c7e 100644 --- a/src/Spectre.Console/Widgets/Table/TableRenderer.cs +++ b/src/Spectre.Console/Widgets/Table/TableRenderer.cs @@ -163,7 +163,7 @@ namespace Spectre.Console return Array.Empty(); } - var paragraph = new Markup(header.Text.Capitalize(), header.Style ?? defaultStyle) + var paragraph = new Markup(header.Text.CapitalizeFirstLetter(), header.Style ?? defaultStyle) .Alignment(Justify.Center) .Overflow(Overflow.Ellipsis);