From 4afbecb1e78f55f430cf12a40429666ee3af18a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:29:25 +0000 Subject: [PATCH 1/3] Initial plan From 36d13772dbdc9047b05476f2e46a40267c83161a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:40:16 +0000 Subject: [PATCH 2/3] Implement graph widget with SVG area chart rendering Co-authored-by: AlexHedley <1573469+AlexHedley@users.noreply.github.com> Agent-Logs-Url: https://github.com/AlexHedley/blashing/sessions/c1190fda-b917-4ea5-894f-fbd50429d958 --- src/Blashing.Core.Tests/GraphWidgetTest.cs | 86 +++++++++++++++++-- .../Components/Graph/GraphWidget.razor | 13 ++- .../Components/Graph/GraphWidget.razor.cs | 56 +++++++++++- .../Components/Graph/GraphWidget.razor.css | 2 + .../Components/Graph/GraphWidget.razor.scss | 2 + src/Blashing.Shared/Pages/Demo.razor | 3 +- src/Blashing.Shared/Pages/Index.razor | 3 +- .../Stories/GraphWidget.stories.razor | 12 ++- 8 files changed, 161 insertions(+), 16 deletions(-) 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.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 @@ + + + - - + + + From 5a0c629f1b9458915a88b0276f71537f9668a9fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:50:59 +0000 Subject: [PATCH 3/3] Add graph widget screenshot to Shared project images Co-authored-by: AlexHedley <1573469+AlexHedley@users.noreply.github.com> Agent-Logs-Url: https://github.com/AlexHedley/blashing/sessions/f8c17b42-fab6-4621-b30e-994316c03f0e --- .../wwwroot/images/graph-widget.png | Bin 0 -> 10081 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/Blashing.Shared/wwwroot/images/graph-widget.png 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 0000000000000000000000000000000000000000..b174676dfcb13be09bea1ab15d6ef2f1dde440dd GIT binary patch literal 10081 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 e@c-?ExS#