diff --git a/src/Blashing.Core.Tests/GraphWidgetTest.cs b/src/Blashing.Core.Tests/GraphWidgetTest.cs index 82bba73..3f541f7 100644 --- a/src/Blashing.Core.Tests/GraphWidgetTest.cs +++ b/src/Blashing.Core.Tests/GraphWidgetTest.cs @@ -7,7 +7,7 @@ namespace Blashing.Core.Tests; public class GraphWidgetTest : BunitContext { [Fact] - public void CommentsWidgetMarkupShouldContainPassedInValues() + public void GraphWidgetMarkupShouldContainPassedInValues() { var title = "Graph 1"; var moreInfo = "This graph contains lots of useful data"; @@ -17,11 +17,6 @@ public void CommentsWidgetMarkupShouldContainPassedInValues() .Add(p => p.MoreInfo, moreInfo) ); - //var widget = @" - //

@Title

- //

- //

@MoreInfo

"; - var expectedTitleMarkup = $"

{title}

"; cut.FindAll("h1")[0].MarkupMatches(expectedTitleMarkup); @@ -30,7 +25,7 @@ public void CommentsWidgetMarkupShouldContainPassedInValues() } [Fact] - public void CommentsWidgetShouldContainPassedInValues() + public void GraphWidgetShouldContainPassedInValues() { var title = "Graph 1"; var moreInfo = "This graph contains lots of useful data"; @@ -44,4 +39,81 @@ public void CommentsWidgetShouldContainPassedInValues() Assert.Equal(graphWidget.Title, title); Assert.Equal(graphWidget.MoreInfo, moreInfo); } + + [Fact] + public void GraphWidgetWithCurrentValueShouldRenderValue() + { + var title = "Graph 1"; + var current = "42"; + var prefix = "$"; + var suffix = "k"; + + var cut = Render(parameters => parameters + .Add(p => p.Title, title) + .Add(p => p.Current, current) + .Add(p => p.Prefix, prefix) + .Add(p => p.Suffix, suffix) + ); + + var graphWidget = cut.Instance; + Assert.Equal(graphWidget.Current, current); + Assert.Equal(graphWidget.Prefix, prefix); + Assert.Equal(graphWidget.Suffix, suffix); + + var h2 = cut.FindAll("h2")[0]; + h2.MarkupMatches($"

{prefix}{current}{suffix}

"); + } + + [Fact] + public void GraphWidgetWithPointsShouldRenderSvg() + { + var points = new List { 10, 20, 15, 30, 25 }; + + var cut = Render(parameters => parameters + .Add(p => p.Title, "Graph") + .Add(p => p.Points, points) + ); + + var graphWidget = cut.Instance; + Assert.Equal(graphWidget.Points, points); + + var svgs = cut.FindAll("svg"); + Assert.Single(svgs); + } + + [Fact] + public void GraphWidgetWithoutPointsShouldNotRenderSvg() + { + var cut = Render(parameters => parameters + .Add(p => p.Title, "Graph") + ); + + var svgs = cut.FindAll("svg"); + Assert.Empty(svgs); + } + + [Fact] + public void GenerateSvgPathShouldReturnEmptyForFewerThanTwoPoints() + { + var cut = Render(parameters => parameters + .Add(p => p.Points, new List { 42 }) + ); + + var path = cut.Instance.GenerateSvgPath(); + Assert.Equal(string.Empty, path); + } + + [Fact] + public void GenerateSvgPathShouldReturnValidPathForTwoPoints() + { + var cut = Render(parameters => parameters + .Add(p => p.Points, new List { 10, 20 }) + ); + + var path = cut.Instance.GenerateSvgPath(); + Assert.NotEmpty(path); + Assert.StartsWith("M ", path); + Assert.Contains(" C ", path); + Assert.EndsWith(" Z", path); + } } \ No newline at end of file diff --git a/src/Blashing.Core/Components/Graph/GraphWidget.razor b/src/Blashing.Core/Components/Graph/GraphWidget.razor index fa9c17a..9af39fa 100644 --- a/src/Blashing.Core/Components/Graph/GraphWidget.razor +++ b/src/Blashing.Core/Components/Graph/GraphWidget.razor @@ -1,9 +1,20 @@ @inherits BaseWidget;
+ @if (Points != null && Points.Any()) + { + + + + } +

@Title

-

+ @if (!string.IsNullOrEmpty(Current)) + { +

@Prefix@Current@Suffix

+ }

@MoreInfo

diff --git a/src/Blashing.Core/Components/Graph/GraphWidget.razor.cs b/src/Blashing.Core/Components/Graph/GraphWidget.razor.cs index 4765dce..5d7b397 100644 --- a/src/Blashing.Core/Components/Graph/GraphWidget.razor.cs +++ b/src/Blashing.Core/Components/Graph/GraphWidget.razor.cs @@ -1,3 +1,4 @@ +using System.Text; using Microsoft.AspNetCore.Components; namespace Blashing.Core.Components.Graph; @@ -7,11 +8,62 @@ public partial class GraphWidget : BaseWidget [Parameter] public string? Title { get; set; } + [Parameter] + public string? Current { get; set; } + + [Parameter] + public string? Prefix { get; set; } + + [Parameter] + public string? Suffix { get; set; } + [Parameter] public string? MoreInfo { get; set; } - + + [Parameter] + public IEnumerable Points { get; set; } = []; + protected override void OnParametersSet() { - BackgroundColor ??= "#ff9618"; + // #dc5945 matches the $background-color in GraphWidget.razor.scss + BackgroundColor ??= "#dc5945"; + } + + private const int SvgViewBoxWidth = 300; + private const int SvgViewBoxHeight = 200; + private const double SvgTopPadding = 10; + + public string GenerateSvgPath() + { + var pointsList = Points?.ToList(); + if (pointsList == null || pointsList.Count < 2) + return string.Empty; + + var min = pointsList.Min(); + var max = pointsList.Max(); + var range = max - min; + if (range == 0) range = 1; + + var n = pointsList.Count; + var coords = pointsList.Select((y, i) => ( + x: (double)i / (n - 1) * SvgViewBoxWidth, + y: (SvgViewBoxHeight - SvgTopPadding) - ((y - min) / range) * (SvgViewBoxHeight - SvgTopPadding) + )).ToList(); + + var sb = new StringBuilder(); + sb.Append(FormattableString.Invariant($"M {coords[0].x:F1} {coords[0].y:F1}")); + + for (int i = 0; i < coords.Count - 1; i++) + { + double midX = (coords[i].x + coords[i + 1].x) / 2; + sb.Append(FormattableString.Invariant( + $" C {midX:F1} {coords[i].y:F1}, {midX:F1} {coords[i + 1].y:F1}, {coords[i + 1].x:F1} {coords[i + 1].y:F1}")); + } + + sb.Append(FormattableString.Invariant($" L {coords[^1].x:F1} {SvgViewBoxHeight}.0")); + sb.Append(FormattableString.Invariant($" L {coords[0].x:F1} {SvgViewBoxHeight}.0")); + sb.Append(" Z"); + + return sb.ToString(); } } \ No newline at end of file diff --git a/src/Blashing.Core/Components/Graph/GraphWidget.razor.css b/src/Blashing.Core/Components/Graph/GraphWidget.razor.css index 6b8c3a6..e60bafb 100644 --- a/src/Blashing.Core/Components/Graph/GraphWidget.razor.css +++ b/src/Blashing.Core/Components/Graph/GraphWidget.razor.css @@ -20,6 +20,8 @@ svg { fill-opacity: 0.4; left: 0px; top: 0px; + width: 100%; + height: 100%; } .title, .value { diff --git a/src/Blashing.Core/Components/Graph/GraphWidget.razor.scss b/src/Blashing.Core/Components/Graph/GraphWidget.razor.scss index 099d5af..1598bbb 100644 --- a/src/Blashing.Core/Components/Graph/GraphWidget.razor.scss +++ b/src/Blashing.Core/Components/Graph/GraphWidget.razor.scss @@ -22,6 +22,8 @@ $tick-color: rgba(0, 0, 0, 0.4); fill-opacity: 0.4; left: 0px; top: 0px; + width: 100%; + height: 100%; } .title, .value { diff --git a/src/Blashing.Shared/Pages/Demo.razor b/src/Blashing.Shared/Pages/Demo.razor index e251578..7007abb 100644 --- a/src/Blashing.Shared/Pages/Demo.razor +++ b/src/Blashing.Shared/Pages/Demo.razor @@ -34,7 +34,7 @@ @*
*@ - +
@@ -45,4 +45,5 @@ @code { List<(string label, string value)>? Items = new() { ("Turn-key", "0"), ("Synergy", "4"), ("Exit strategy", "11"), ("Paridigm shift", "3"), ("Enterprise", "20"), ("Pivoting", "7"), ("Web 2.0", "16"), ("Leverage", "25"), ("Streamlininess", "9") }; + List GraphPoints = new() { 10, 30, 25, 50, 45, 70, 60, 80, 65 }; } \ No newline at end of file diff --git a/src/Blashing.Shared/Pages/Index.razor b/src/Blashing.Shared/Pages/Index.razor index 90d34d8..f783d9a 100644 --- a/src/Blashing.Shared/Pages/Index.razor +++ b/src/Blashing.Shared/Pages/Index.razor @@ -16,7 +16,7 @@
- +
@@ -42,4 +42,5 @@ @code { List<(string label, string value)>? Items = new() { ("a", "b"), ("c", "d") }; + List GraphPoints = new() { 10, 30, 25, 50, 45, 70, 60, 80, 65 }; } \ No newline at end of file diff --git a/src/Blashing.Shared/wwwroot/images/graph-widget.png b/src/Blashing.Shared/wwwroot/images/graph-widget.png new file mode 100644 index 0000000..b174676 Binary files /dev/null and b/src/Blashing.Shared/wwwroot/images/graph-widget.png differ diff --git a/src/Blashing.Stories/Stories/GraphWidget.stories.razor b/src/Blashing.Stories/Stories/GraphWidget.stories.razor index 0dcb1e2..62a3331 100644 --- a/src/Blashing.Stories/Stories/GraphWidget.stories.razor +++ b/src/Blashing.Stories/Stories/GraphWidget.stories.razor @@ -5,19 +5,23 @@ + + + - - + + +