diff --git a/src/Blashing.Core.Tests/MeterWidgetTest.cs b/src/Blashing.Core.Tests/MeterWidgetTest.cs index 67ce368..c4254ca 100644 --- a/src/Blashing.Core.Tests/MeterWidgetTest.cs +++ b/src/Blashing.Core.Tests/MeterWidgetTest.cs @@ -34,21 +34,10 @@ public void MeterWidgetMarkupShouldContainPassedInValues() .Add(p => p.Min, min) .Add(p => p.Max, max) ); - - //var widget = @" - //
- //

a

- // - //

c

- //

d

- //
"; var expectedTitleMarkup = $"

{title}

"; cut.FindAll("h1")[0].MarkupMatches(expectedTitleMarkup); - // var expectedInputMarkup = $""; - // cut.FindAll("input")[0].MarkupMatches(expectedInputMarkup); - var expectedMoreInfoMarkup = $"

{moreInfo}

"; cut.FindAll("p")[0].MarkupMatches(expectedMoreInfoMarkup); @@ -98,4 +87,138 @@ public void MeterWidgetShouldContainPassedInValues() Assert.Equal(meterWidget.Min, min); Assert.Equal(meterWidget.Max, max); } + + [Fact] + public void MeterWidgetShouldShowSvgGauge() + { + var cut = Render(parameters => parameters + .Add(p => p.Title, "Test") + .Add(p => p.Value, 50) + .Add(p => p.Min, 0) + .Add(p => p.Max, 100) + ); + + var svg = cut.Find("svg.meter"); + Assert.NotNull(svg); + + var backgroundPath = cut.Find("path.meter-background"); + Assert.NotNull(backgroundPath); + Assert.False(string.IsNullOrEmpty(backgroundPath.GetAttribute("d"))); + + var valuePath = cut.Find("path.meter-value"); + Assert.NotNull(valuePath); + Assert.False(string.IsNullOrEmpty(valuePath.GetAttribute("d"))); + } + + [Fact] + public void MeterWidgetShouldHideValueArcWhenValueEqualsMin() + { + var cut = Render(parameters => parameters + .Add(p => p.Value, 0) + .Add(p => p.Min, 0) + .Add(p => p.Max, 100) + ); + + var valuePaths = cut.FindAll("path.meter-value"); + Assert.Empty(valuePaths); + } + + [Fact] + public void MeterWidgetShouldShowCenterTextWhenDisplayInputIsTrue() + { + var cut = Render(parameters => parameters + .Add(p => p.Value, 42) + .Add(p => p.Min, 0) + .Add(p => p.Max, 100) + .Add(p => p.DisplayInput, true) + ); + + var text = cut.Find("text.meter-text"); + Assert.NotNull(text); + Assert.Contains("42", text.TextContent); + } + + [Fact] + public void MeterWidgetShouldHideCenterTextWhenDisplayInputIsFalse() + { + var cut = Render(parameters => parameters + .Add(p => p.Value, 42) + .Add(p => p.Min, 0) + .Add(p => p.Max, 100) + .Add(p => p.DisplayInput, false) + ); + + var texts = cut.FindAll("text.meter-text"); + Assert.Empty(texts); + } + + [Fact] + public void MeterWidgetShouldShowPrefixAndSuffix() + { + var cut = Render(parameters => parameters + .Add(p => p.Value, 75) + .Add(p => p.Min, 0) + .Add(p => p.Max, 100) + .Add(p => p.Prefix, "$") + .Add(p => p.Suffix, "%") + .Add(p => p.DisplayInput, true) + ); + + var text = cut.Find("text.meter-text"); + Assert.Contains("$", text.TextContent); + Assert.Contains("75", text.TextContent); + Assert.Contains("%", text.TextContent); + } + + [Fact] + public void MeterWidgetDefaultsShouldProduceValidSvgPaths() + { + var cut = Render(parameters => parameters + .Add(p => p.Value, 75) + ); + + var meterWidget = cut.Instance; + Assert.Equal(-125, meterWidget.AngleOffset); + Assert.Equal(250, meterWidget.AngleArc); + Assert.Equal(200, meterWidget.Width); + Assert.Equal(200, meterWidget.Height); + Assert.Equal(100, meterWidget.Max); + + var backgroundPath = cut.Find("path.meter-background"); + var d = backgroundPath.GetAttribute("d"); + Assert.False(string.IsNullOrEmpty(d)); + Assert.StartsWith("M ", d); + + var valuePath = cut.Find("path.meter-value"); + var vd = valuePath.GetAttribute("d"); + Assert.False(string.IsNullOrEmpty(vd)); + Assert.StartsWith("M ", vd); + } + + [Theory] + [InlineData(0, 0, 100)] // at min: no value arc + [InlineData(50, 0, 100)] // mid value + [InlineData(100, 0, 100)] // at max + public void MeterWidgetArcRendersCorrectlyForValues(long value, long min, long max) + { + var cut = Render(parameters => parameters + .Add(p => p.Value, value) + .Add(p => p.Min, min) + .Add(p => p.Max, max) + ); + + var backgroundPath = cut.Find("path.meter-background"); + Assert.False(string.IsNullOrEmpty(backgroundPath.GetAttribute("d"))); + + if (value > min) + { + var valuePath = cut.Find("path.meter-value"); + Assert.NotNull(valuePath); + } + else + { + var valuePaths = cut.FindAll("path.meter-value"); + Assert.Empty(valuePaths); + } + } } \ No newline at end of file diff --git a/src/Blashing.Core/Components/Meter/MeterWidget.razor b/src/Blashing.Core/Components/Meter/MeterWidget.razor index 2e6ac34..a71ca6b 100644 --- a/src/Blashing.Core/Components/Meter/MeterWidget.razor +++ b/src/Blashing.Core/Components/Meter/MeterWidget.razor @@ -2,18 +2,28 @@

@Title

- - - + + + + @if (Value > Min) + { + + } + @if (DisplayInput) + { + @GetCenterTextMarkup() + } + +

@MoreInfo

- +

@UpdatedAtMessage

\ No newline at end of file diff --git a/src/Blashing.Core/Components/Meter/MeterWidget.razor.cs b/src/Blashing.Core/Components/Meter/MeterWidget.razor.cs index 9114893..df893a5 100644 --- a/src/Blashing.Core/Components/Meter/MeterWidget.razor.cs +++ b/src/Blashing.Core/Components/Meter/MeterWidget.razor.cs @@ -12,33 +12,98 @@ public partial class MeterWidget : BaseWidget [Parameter] public string? UpdatedAtMessage { get; set; } - + [Parameter] - public long AngleOffset { get; set; } - + public long AngleOffset { get; set; } = -125; + [Parameter] - public long AngleArc { get; set; } - + public long AngleArc { get; set; } = 250; + [Parameter] - public long Height { get; set; } - + public long Height { get; set; } = 200; + [Parameter] - public long Width { get; set; } - + public long Width { get; set; } = 200; + [Parameter] - public bool ReadOnly { get; set; } - + public bool ReadOnly { get; set; } = true; + [Parameter] public long Value { get; set; } - + [Parameter] public long Min { get; set; } - + + [Parameter] + public long Max { get; set; } = 100; + + [Parameter] + public bool DisplayInput { get; set; } = true; + + [Parameter] + public string? Prefix { get; set; } + [Parameter] - public long Max { get; set; } - + public string? Suffix { get; set; } + protected override void OnParametersSet() { BackgroundColor ??= "#9c4274"; } + + internal double Radius => Math.Min(Width, Height) / 2.0 * 0.8; + + internal string CenterX => + (Width / 2.0).ToString("F2", System.Globalization.CultureInfo.InvariantCulture); + + internal string CenterY => + (Height / 2.0).ToString("F2", System.Globalization.CultureInfo.InvariantCulture); + + internal string StrokeWidth => + (Math.Min(Width, Height) * 0.1).ToString("F2", System.Globalization.CultureInfo.InvariantCulture); + + internal string FontSize => + (Math.Min(Width, Height) * 0.2).ToString("F2", System.Globalization.CultureInfo.InvariantCulture); + + internal string GetBackgroundArcPath() + { + double cx = Width / 2.0; + double cy = Height / 2.0; + return GetArcPath(cx, cy, Radius, AngleOffset, AngleOffset + AngleArc); + } + + internal string GetValueArcPath() + { + double cx = Width / 2.0; + double cy = Height / 2.0; + double ratio = Max > Min + ? Math.Clamp((double)(Value - Min) / (Max - Min), 0.0, 1.0) + : 0.0; + double valueAngle = AngleOffset + ratio * AngleArc; + return GetArcPath(cx, cy, Radius, AngleOffset, valueAngle); + } + + internal MarkupString GetCenterTextMarkup() + { + string encodedPrefix = System.Net.WebUtility.HtmlEncode(Prefix ?? string.Empty); + string encodedSuffix = System.Net.WebUtility.HtmlEncode(Suffix ?? string.Empty); + return (MarkupString)FormattableString.Invariant( + $"{encodedPrefix}{Value}{encodedSuffix}"); + } + + internal static string GetArcPath(double cx, double cy, double r, double startDeg, double endDeg) + { + double startRad = (startDeg - 90.0) * Math.PI / 180.0; + double endRad = (endDeg - 90.0) * Math.PI / 180.0; + + double x1 = cx + r * Math.Cos(startRad); + double y1 = cy + r * Math.Sin(startRad); + double x2 = cx + r * Math.Cos(endRad); + double y2 = cy + r * Math.Sin(endRad); + + int largeArcFlag = Math.Abs(endDeg - startDeg) > 180 ? 1 : 0; + + return FormattableString.Invariant( + $"M {x1:F2},{y1:F2} A {r:F2},{r:F2} 0 {largeArcFlag} 1 {x2:F2},{y2:F2}"); + } } \ No newline at end of file diff --git a/src/Blashing.Core/Components/Meter/MeterWidget.razor.css b/src/Blashing.Core/Components/Meter/MeterWidget.razor.css index c215c8a..8a381ac 100644 --- a/src/Blashing.Core/Components/Meter/MeterWidget.razor.css +++ b/src/Blashing.Core/Components/Meter/MeterWidget.razor.css @@ -1,22 +1,30 @@ .widget-meter { - /*background-color: #9c4274;*/ height: 100%; width: 100%; text-align: center; - /* width: inherit;*/ - /* height: inherit;*/ - /* display: table-cell;*/ vertical-align: middle; display: flex; flex-direction: column; justify-content: center; - /*align-items: center;*/ + align-items: center; } -input.meter { - background-color: #9c4274; /*darken(#9c4274, 15%);*/ - color: #fff; - filter: brightness(0.85); +svg.meter { + overflow: visible; +} + +path.meter-background { + fill: none; + stroke: rgba(0, 0, 0, 0.2); +} + +path.meter-value { + fill: none; + stroke: rgba(255, 255, 255, 0.8); +} + +text.meter-text { + fill: white; } .title { diff --git a/src/Blashing.Shared/Pages/Demo.razor b/src/Blashing.Shared/Pages/Demo.razor index e251578..dc131bf 100644 --- a/src/Blashing.Shared/Pages/Demo.razor +++ b/src/Blashing.Shared/Pages/Demo.razor @@ -19,7 +19,7 @@ @*
*@ - +
diff --git a/src/Blashing.Stories/Stories/MeterWidget.stories.razor b/src/Blashing.Stories/Stories/MeterWidget.stories.razor index 1fb5978..a10684e 100644 --- a/src/Blashing.Stories/Stories/MeterWidget.stories.razor +++ b/src/Blashing.Stories/Stories/MeterWidget.stories.razor @@ -7,6 +7,12 @@ + + + + + + @@ -14,12 +20,33 @@ + + + + + + + + + + + + + + + + + + + + +