From 697869f7e8572cab0058d8f730e10799aa3dd664 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 4 Jun 2026 13:09:37 -0400 Subject: [PATCH 01/16] Bump module version to 0.3.3, update dependencies, and enhance release workflow for Bluesky posting --- .github/workflows/Release.yml | 45 +++++++------------- AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 | 2 +- CHANGELOG.md | 10 ++++- Sources/AsBuiltReportChart.csproj | 8 ++-- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 79a0a0b..6496cc2 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -2,7 +2,7 @@ name: Publish PowerShell Module on: release: - types: [ published ] + types: [published] jobs: publish-to-psgallery: @@ -23,12 +23,14 @@ jobs: - name: Build the library for MacOS Arm64 run: dotnet publish ./Sources -c Release -r osx-arm64 - name: Copy the library for MacOS Arm64 - run: copy ./Sources/bin/Release/netstandard2.0/osx-arm64/publish/*.* + run: + copy ./Sources/bin/Release/netstandard2.0/osx-arm64/publish/*.* ./AsBuiltReport.Chart/Src/Assemblies/Core/mac-osx/osx-arm64 - name: Build the library for Linux run: dotnet publish ./Sources -c Release -r linux-x64 - name: Copy the library for Linux - run: copy ./Sources/bin/Release/netstandard2.0/linux-x64/publish/*.* + run: + copy ./Sources/bin/Release/netstandard2.0/linux-x64/publish/*.* ./AsBuiltReport.Chart/Src/Assemblies/Core/linux-x64 - name: Build the library for Windows run: dotnet publish ./Sources -c Release -r win-x64 @@ -54,30 +56,13 @@ jobs: shell: pwsh run: | Publish-Module -Path .\AsBuiltReport.Chart\ -NuGetApiKey ${{ secrets.PSGALLERY_API_KEY }} -Verbose - # tweet: - # needs: publish-to-psgallery - # runs-on: ubuntu-latest - # steps: - # - uses: Eomm/why-don-t-you-tweet@v2 - # # We don't want to tweet if the repository is not a public one - # if: ${{ !github.event.repository.private }} - # with: - # # GitHub event payload - # # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release - # tweet-message: "[New Release] ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}! Check out what's new! ${{ github.event.release.html_url }} #AsBuiltReport #PowerShell" - # env: - # TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} - # TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} - # TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} - # TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} - # bsky-post: - # needs: publish-to-psgallery - # runs-on: ubuntu-latest - # steps: - # - uses: zentered/bluesky-post-action@v0.3.0 - # with: - # post: "[New Release] ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}! Check out what's new! ${{ github.event.release.html_url }} #AsBuiltReport #PowerShell" - # env: - # BSKY_IDENTIFIER: ${{ secrets.BSKY_IDENTIFIER }} - # BSKY_PASSWORD: ${{ secrets.BSKY_PASSWORD }} - + bsky-post: + needs: publish-to-psgallery + runs-on: ubuntu-latest + steps: + - uses: zentered/bluesky-post-action@v0.4.0 + with: + post: "[New Release] ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}! Check out what's new! ${{ github.event.release.html_url }} #AsBuiltReport #PowerShell" + env: + BSKY_IDENTIFIER: ${{ secrets.BSKY_IDENTIFIER }} + BSKY_PASSWORD: ${{ secrets.BSKY_PASSWORD }} diff --git a/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 b/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 index f37ef90..bc8df44 100644 --- a/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 +++ b/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 @@ -12,7 +12,7 @@ RootModule = 'AsBuiltReport.Chart.psm1' # Version number of this module. - ModuleVersion = '0.3.2' + ModuleVersion = '0.3.3' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ca8ca..4e8c3de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.3.2] - 2026-05-05 +## [0.3.3] - Unreleased ### Added @@ -16,6 +16,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update module version to 0.3.2 +## [0.3.2] - 2026-05-05 + +### Changed + +- Update module v0.3.3 +- Update SkiaSharp .NET dependency to v3.119.4 +- Update HarfBuzzSharp .NET dependency to v8.3.1.5 + ## [0.3.1] - 2026-04-23 ### Changed diff --git a/Sources/AsBuiltReportChart.csproj b/Sources/AsBuiltReportChart.csproj index 5a0269a..94f1aa2 100644 --- a/Sources/AsBuiltReportChart.csproj +++ b/Sources/AsBuiltReportChart.csproj @@ -3,15 +3,15 @@ netstandard2.0 - 0.3.2 + 0.3.3 - + - - + + From 1f6120f6370750c997e6839844d42bfde407b519 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 4 Jun 2026 21:06:19 -0400 Subject: [PATCH 02/16] Add Donut Chart functionality and examples; update module exports and cmdlets --- AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 | 2 +- CHANGELOG.md | 6 +- Examples/Example12.ps1 | 61 ++++ Examples/Example13.ps1 | 98 +++++++ Sources/Chart.cs | 25 +- Sources/DonutChart.cs | 132 +++++++++ Sources/PieChart.cs | 6 +- Sources/PowerShell/BarChartPwsh.cs | 1 + Sources/PowerShell/DonutChartPwsh.cs | 271 ++++++++++++++++++ Sources/PowerShell/SignalChartPwsh.cs | 1 + .../PowerShell/SingleStackedBarChartPwsh.cs | 1 + Sources/PowerShell/StackedBarChartPwsh.cs | 1 + 12 files changed, 597 insertions(+), 8 deletions(-) create mode 100644 Examples/Example12.ps1 create mode 100644 Examples/Example13.ps1 create mode 100644 Sources/DonutChart.cs create mode 100644 Sources/PowerShell/DonutChartPwsh.cs diff --git a/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 b/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 index bc8df44..9ab6ce1 100644 --- a/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 +++ b/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 @@ -69,7 +69,7 @@ # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = 'New-PieChart', 'New-BarChart', 'New-StackedBarChart', 'New-SignalChart', 'New-SingleStackedBarChart' + FunctionsToExport = 'New-PieChart', 'New-BarChart', 'New-StackedBarChart', 'New-SignalChart', 'New-SingleStackedBarChart', 'New-DonutChart' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. # CmdletsToExport = '*' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e8c3de..fb4b8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add New-SingleStackedBarChart cmdlet and update module exports -- Add pester test to validate the functionality of the New-SingleStackedBarChart cmdlet +- Add New-DonutChart cmdlet and update module exports +- Add pester test to validate the functionality of the New-DonutChart cmdlet ### Changed -- Update module version to 0.3.2 +- Update module version to 0.3.3 ## [0.3.2] - 2026-05-05 diff --git a/Examples/Example12.ps1 b/Examples/Example12.ps1 new file mode 100644 index 0000000..25b5a1f --- /dev/null +++ b/Examples/Example12.ps1 @@ -0,0 +1,61 @@ +<# + .SYNOPSIS + Example 01 - Basic Donut Chart + + .DESCRIPTION + This example demonstrates how to create a basic Donut Chart using the AsBuiltReport.Chart module. + The chart displays a simple breakdown of VM power states across a vSphere environment. +#> + +[CmdletBinding()] +param ( + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, + [string] $Format = 'png' +) + +<# + Starting with PowerShell v3, modules are auto-imported when needed. Importing the module here + ensures clarity and avoids ambiguity. +#> + +# Import-Module AsBuiltReport.Chart -Force -Verbose:$false + +<# + Since the chart output is a file, specify the output folder path using $OutputFolderPath. +#> + +$OutputFolderPath = Resolve-Path $Path + +<# + Define the data to be displayed in the chart. + In a real-world scenario these values would come from your infrastructure query. +#> + +$ChartTitle = 'User Type Breakdown' +$Values = @(800, 80, 200) +$Labels = @('Members', 'Guests', 'Disabled') + +<# + The New-DonutChart cmdlet generates the Donut Chart image. + + -Title : Sets the chart title displayed at the top of the image. + -Values : Array of numeric values, one per slice. + -Labels : Array of label strings corresponding to each value. + -Format : Output file format (e.g. png, jpg, svg). + -OutputFolderPath : Directory where the generated chart file will be saved. + -Width : Width of the chart image in pixels. + -Height : Height of the chart image in pixels. + -ColorPalette : Predefined color palette (e.g. Category20, Pastel). + -Filename : Name of the output file (without extension). +#> + +New-DonutChart ` + -Title $ChartTitle ` + -Values $Values ` + -Labels $Labels ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Width 600 ` + -Height 400 ` + -ColorPalette Category20 ` + -Filename 'Example12-DonutChart' diff --git a/Examples/Example13.ps1 b/Examples/Example13.ps1 new file mode 100644 index 0000000..908cc57 --- /dev/null +++ b/Examples/Example13.ps1 @@ -0,0 +1,98 @@ +<# + .SYNOPSIS + Example 02 - Donut Chart with Legend, Custom Colors and Border + + .DESCRIPTION + This example demonstrates how to create a Donut Chart with additional visual options, including: + - An enabled legend with custom alignment and orientation + - A custom hex color palette + - A chart border + - Adjusted title and label font sizes + - Custom chart dimensions + + The chart displays a server operating system distribution report. +#> + +[CmdletBinding()] +param ( + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, + [string] $Format = 'png' +) + +<# + Starting with PowerShell v3, modules are auto-imported when needed. Importing the module here + ensures clarity and avoids ambiguity. +#> + +# Import-Module AsBuiltReport.Chart -Force -Verbose:$false + +<# + Since the chart output is a file, specify the output folder path using $OutputFolderPath. +#> + +$OutputFolderPath = Resolve-Path $Path + +<# + Define the data to be displayed in the chart. + In a real-world scenario these values would come from your infrastructure query. +#> + +$ChartTitle = 'Server OS Distribution' +$Values = @(85, 60, 30, 15) +$Labels = @('Windows Server 2022', 'Windows Server 2019', 'RHEL 9', 'Ubuntu 22.04') + +<# + A custom hex color palette can be used to match corporate branding or improve readability. + Each color corresponds to a slice in the order they appear in $Values. +#> + +$CustomColors = @('#0078D4', '#00B7C3', '#E74C3C', '#F39C12') + +<# + The New-DonutChart cmdlet generates the Donut Chart image. + + -Title : Sets the chart title. + -TitleFontSize : Sets the font size of the title in points. + -TitleFontBold : Renders the title in bold. + -Values : Array of numeric values, one per slice. + -Labels : Array of label strings corresponding to each value. + -LabelFontSize : Sets the font size of the slice labels. + -LabelDistance : Controls how far labels are placed from the chart center (0.5-0.9). + -EnableLegend : Enables the legend on the chart. + -LegendAlignment : Positions the legend (e.g. UpperRight, LowerCenter). + -LegendOrientation : Sets legend layout direction (Vertical or Horizontal). + -LegendFontSize : Sets the legend text font size in points. + -EnableChartBorder : Draws a border around the chart area. + -ChartBorderColor : Sets the border color. + -ChartBorderSize : Sets the border thickness in pixels. + -EnableCustomColorPalette : Enables use of the custom color palette. + -CustomColorPalette : Array of hex color strings for each slice. + -Width : Output image width in pixels. + -Height : Output image height in pixels. + -Format : Output file format (e.g. png, jpg, svg). + -OutputFolderPath : Directory where the generated chart file will be saved. + -Filename : Name of the output file (without extension). +#> + +New-DonutChart ` + -Title $ChartTitle ` + -TitleFontSize 18 ` + -TitleFontBold ` + -Values $Values ` + -Labels $Labels ` + -LabelFontSize 13 ` + -LabelDistance 0.7 ` + -EnableLegend ` + -LegendAlignment UpperRight ` + -LegendOrientation Vertical ` + -LegendFontSize 12 ` + -EnableChartBorder ` + -ChartBorderColor Black ` + -ChartBorderSize 2 ` + -EnableCustomColorPalette ` + -CustomColorPalette $CustomColors ` + -Width 600 ` + -Height 400 ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Filename 'Example13-DonutChart-Advanced' diff --git a/Sources/Chart.cs b/Sources/Chart.cs index 4c5834d..cb260b1 100644 --- a/Sources/Chart.cs +++ b/Sources/Chart.cs @@ -33,7 +33,7 @@ internal partial class Chart public static string LabelXAxis { get; set; } = "Values"; // this set the distance of the labels from the chart center (Pie Chart) - internal static double _labelDistance = 0.6; + internal static double _labelDistance; public static double LabelDistance { get { return _labelDistance; } @@ -71,6 +71,24 @@ public static double AreaExplodeFraction } } + // this set the distance of the chart area elements (Donut Chart) + internal static double _donutFraction; + public static double DonutFraction + { + get { return _donutFraction; } + set + { + if (value >= 0.0 && value <= 0.5) + { + _donutFraction = value; + } + else + { + throw new ArgumentException("Error: DonutFraction value range must be from 0.0 to 0.5."); + } + } + } + // Legend setting (Pie Chart) public static bool EnableLegend { get; set; } @@ -87,6 +105,9 @@ public static double AreaExplodeFraction public static Alignments LegendAlignment { get; set; } = Alignments.LowerRight; + // Hide Values + public static bool HideValues { get; set; } + // Chart border settings (All Charts) public static bool EnableChartBorder { get; set; } public static BorderStyles ChartBorderStyle { get; set; } @@ -439,6 +460,8 @@ internal static void Reset() WatermarkFontSize = 24; WatermarkColor = BasicColors.Gray; _watermarkOpacity = 0.3; + DonutFraction = 0.5; + HideValues = false; } public static string GenerateToken(Byte length) diff --git a/Sources/DonutChart.cs b/Sources/DonutChart.cs new file mode 100644 index 0000000..a6464ac --- /dev/null +++ b/Sources/DonutChart.cs @@ -0,0 +1,132 @@ +using ScottPlot; +using System; +using System.IO; +using System.Collections.Generic; +namespace AsBuiltReportChart +{ + internal class Donut : Chart + { + public Donut() { } + public object Chart(double[] values, string[] labels, string filename = "output", int width = 400, int height = 300) + { + if (values.Length == labels.Length) + { + Plot myPlot = new Plot(); + + if (EnableCustomColorPalette) + { + + if (_customColorPalette != null && _customColorPalette.Length > 0) + { + myPlot.Add.Palette = colorPalette = new ScottPlot.Palettes.Custom(_customColorPalette); + } + else + { + throw new InvalidOperationException("CustomColorPalette is empty. Please provide valid color values."); + } + + } + else + { + // Set ScottPlot native color palette + if (colorPalette != null) + { + myPlot.Add.Palette = colorPalette; + } + } + + List slices = new List(); + for (int i = 0; i < values.Length; i++) + { + slices.Add(new PieSlice() + { + Value = values[i], + LabelText = HideValues ? null : values[i].ToString(), + LabelFontSize = LabelFontSize, + LabelFontColor = GetDrawingColor(LabelFontColor), + LabelBold = LabelBold, + LabelFontName = FontName, + LegendText = EnableLegend ? labels[i] : null, + FillColor = colorPalette.GetColor(i) + }); + } + + var pie = myPlot.Add.Pie(slices); + pie.DonutFraction = DonutFraction; + + // hide unnecessary plot components + myPlot.Axes.Frameless(); + myPlot.HideGrid(); + + if (EnableLegend) + { + // Legend Font Properties + myPlot.Legend.FontName = FontName; + myPlot.Legend.FontSize = LegendFontSize; + myPlot.Legend.FontColor = GetDrawingColor(LegendFontColor); + + // Legend box Style Properties + myPlot.Legend.OutlineColor = GetDrawingColor(LegendBorderColor); + myPlot.Legend.OutlineWidth = LegendBorderSize; + + myPlot.Legend.OutlinePattern = LegendBorderStyleMap[LegendBorderStyle]; + + myPlot.Legend.Orientation = LegendOrientationMap[LegendOrientation]; + + myPlot.Legend.Alignment = LegendAlignmentMap[LegendAlignment]; + } + + if (EnableChartBorder) + { + myPlot.FigureBorder = new LineStyle() + { + Color = GetDrawingColor(ChartBorderColor), + Width = ChartBorderSize, + Pattern = ChartBorderStyleMap[ChartBorderStyle], + }; + } + + // Set title properties + if (Title != null) + { + myPlot.Title(Title); + myPlot.Axes.Title.Label.FontSize = TitleFontSize; + myPlot.Axes.Title.Label.ForeColor = GetDrawingColor(TitleFontColor); + myPlot.Axes.Title.Label.Bold = TitleFontBold; + myPlot.Axes.Title.Label.FontName = FontName; + } + + // Set margins settings + myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); + + // Set background colors + if (FigureBackgroundColor.HasValue) + { + myPlot.FigureBackground.Color = GetDrawingColor(FigureBackgroundColor.Value); + } + if (DataBackgroundColor.HasValue) + { + myPlot.DataBackground.Color = GetDrawingColor(DataBackgroundColor.Value); + } + + // Set label distance from the center of the donut slices + pie.SliceLabelDistance = _labelDistance; + + // Apply watermark if enabled + ApplyWatermark(myPlot); + + // Set filetpath to save + string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); + + // Set filename + return SaveInFormat(myPlot, width, height, Filepath, filename, Format); + } + else + { + throw new ArgumentException("Error: Values and labels must be equal."); + } + } + } +} + + diff --git a/Sources/PieChart.cs b/Sources/PieChart.cs index 12127bb..e2e493d 100644 --- a/Sources/PieChart.cs +++ b/Sources/PieChart.cs @@ -14,7 +14,6 @@ public object Chart(double[] values, string[] labels, string filename = "output" if (EnableCustomColorPalette) { - if (_customColorPalette != null && _customColorPalette.Length > 0) { myPlot.Add.Palette = colorPalette = new ScottPlot.Palettes.Custom(_customColorPalette); @@ -23,7 +22,6 @@ public object Chart(double[] values, string[] labels, string filename = "output" { throw new InvalidOperationException("CustomColorPalette is empty. Please provide valid color values."); } - } else { @@ -36,7 +34,6 @@ public object Chart(double[] values, string[] labels, string filename = "output" var pie = myPlot.Add.Pie(values); pie.ExplodeFraction = _areaExplodeFraction; - pie.SliceLabelDistance = _labelDistance; // set each slice value to its label for (var i = 0; i < pie.Slices.Count; i++) @@ -108,6 +105,9 @@ public object Chart(double[] values, string[] labels, string filename = "output" myPlot.DataBackground.Color = GetDrawingColor(DataBackgroundColor.Value); } + // Set the distance of the chart area elements (Pie Chart) + pie.SliceLabelDistance = _labelDistance; + // Apply watermark if enabled ApplyWatermark(myPlot); diff --git a/Sources/PowerShell/BarChartPwsh.cs b/Sources/PowerShell/BarChartPwsh.cs index 262f0a0..a819429 100644 --- a/Sources/PowerShell/BarChartPwsh.cs +++ b/Sources/PowerShell/BarChartPwsh.cs @@ -161,6 +161,7 @@ public class NewBarChartCommand : Cmdlet // Set OutputFolderPath [Parameter(Mandatory = false, HelpMessage = "Output folder path where the chart will be saved.")] + [ValidatePath()] public string OutputFolderPath { get; set; } = Directory.GetCurrentDirectory(); protected override void ProcessRecord() diff --git a/Sources/PowerShell/DonutChartPwsh.cs b/Sources/PowerShell/DonutChartPwsh.cs new file mode 100644 index 0000000..8daedc5 --- /dev/null +++ b/Sources/PowerShell/DonutChartPwsh.cs @@ -0,0 +1,271 @@ +using System; +using System.IO; +using System.Management.Automation; + +namespace AsBuiltReportChart.PowerShell +{ + [Cmdlet(VerbsCommon.New, "DonutChart")] + public class NewDonutChartCommand : Cmdlet + { + // Declare the parameters for the cmdlet. + [Parameter(Mandatory = false, HelpMessage = "Output filename for the chart. Defaults to a randomly generated 8-character token.")] + public string Filename { get; set; } = Chart.GenerateToken(8); + + [Parameter(Mandatory = true, HelpMessage = "Array of numeric values to display in the donut chart.")] + public double[] Values { get; set; } + + [Parameter(Mandatory = true, HelpMessage = "Array of labels corresponding to each donut slice.")] + public string[] Labels { get; set; } + + [Parameter(Mandatory = true, HelpMessage = "Title text to display on the chart.")] + public string Title { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Switch to make the title font bold.")] + public SwitchParameter TitleFontBold { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Font size for the title. Defaults to 14.")] + public int TitleFontSize { get; set; } = 14; + + [Parameter(Mandatory = false, HelpMessage = "Font color for the title.")] + public BasicColors TitleFontColor { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Switch to enable the legend on the chart.")] + public SwitchParameter EnableLegend { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Orientation of the legend (Vertical or Horizontal). Defaults to Vertical.")] + public Enums.Orientations LegendOrientation { get; set; } = Enums.Orientations.Vertical; + + [Parameter(Mandatory = false, HelpMessage = "Alignment of the legend on the chart. Defaults to UpperRight.")] + public Enums.Alignments LegendAlignment { get; set; } = Enums.Alignments.UpperRight; + + [Parameter(Mandatory = true, HelpMessage = "Output format for the chart (e.g., PNG, JPG, SVG).")] + public Formats Format { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Fraction to explode donut slices (0.0 to 0.5).")] + [ValidateSet("0.0", "0.1", "0.2", "0.3", "0.4", "0.5")] + public double DonutFraction { get; set; } = 0.5; + + [Parameter(Mandatory = false, HelpMessage = "Color of the chart border.")] + public BasicColors ChartBorderColor { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Size of the chart border in pixels. Defaults to 1.")] + public int ChartBorderSize { get; set; } = 1; + + [Parameter(Mandatory = false, HelpMessage = "Style of the chart border.")] + public Enums.BorderStyles ChartBorderStyle { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Switch to enable the chart border.")] + public SwitchParameter EnableChartBorder { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Font size for the legend. Defaults to 14.")] + public int LegendFontSize { get; set; } = 14; + + [Parameter(Mandatory = false, HelpMessage = "Font color for the legend. Defaults to Black.")] + public BasicColors LegendFontColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Border style for the legend.")] + public Enums.BorderStyles LegendBorderStyle { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Border size for the legend in pixels. Defaults to 1.")] + public int LegendBorderSize { get; set; } = 1; + + [Parameter(Mandatory = false, HelpMessage = "Border color for the legend. Defaults to Black.")] + public BasicColors LegendBorderColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Color palette for the chart. Defaults to Category10.")] + public Enums.ColorPalettes ColorPalette { get; set; } = Enums.ColorPalettes.Category10; + + [Parameter(Mandatory = false, HelpMessage = "Switch to enable custom color palette.")] + public SwitchParameter EnableCustomColorPalette { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Invert the custom color palette.")] + public SwitchParameter InvertCustomColorPalette { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Array of custom hex color values for the chart.")] + public string[] CustomColorPalette { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Font name to use for all text. Defaults to Arial.")] + public string FontName { get; set; } = "Arial"; + + // Label Font settings + [Parameter(Mandatory = false, HelpMessage = "Font size for chart labels. Defaults to 14.")] + public int LabelFontSize { get; set; } = 14; + + [Parameter(Mandatory = false, HelpMessage = "Font color for labels. Defaults to Black.")] + public BasicColors LabelFontColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Switch to make label font bold.")] + public SwitchParameter LabelBold { get; set; } + + // this set the distance of the labels from the chart center (Donut Chart) + [Parameter(Mandatory = false, HelpMessage = "Distance of labels from the chart center (0.5 to 0.9). Defaults to 0.6.")] + [ValidateSet("0.5", "0.6", "0.7", "0.8", "0.9")] + public double LabelDistance { get; set; } = 0.7; + + // this set the area axes margins (Bar Chart) + [Parameter(Mandatory = false, HelpMessage = "Top margin for the chart area. Defaults to 0.2.")] + public double AxesMarginsTop { get; set; } = 0.2; + + [Parameter(Mandatory = false, HelpMessage = "Bottom margin for the chart area. Defaults to 0.05.")] + public double AxesMarginsDown { get; set; } = 0.05; + + [Parameter(Mandatory = false, HelpMessage = "Left margin for the chart area. Defaults to 0.05.")] + public double AxesMarginsLeft { get; set; } = 0.05; + + [Parameter(Mandatory = false, HelpMessage = "Right margin for the chart area. Defaults to 0.05.")] + public double AxesMarginsRight { get; set; } = 0.05; + + [Parameter(Mandatory = false, HelpMessage = "Background color of the entire figure (canvas).")] + public BasicColors? FigureBackgroundColor { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Background color of the data area inside the axes.")] + public BasicColors? DataBackgroundColor { get; set; } + + // Watermark settings + [Parameter(Mandatory = false, HelpMessage = "Enable a watermark on the chart.")] + public SwitchParameter EnableWatermark { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Text to display as the watermark. Defaults to 'Confidential'.")] + public string WatermarkText { get; set; } = "Confidential"; + + [Parameter(Mandatory = false, HelpMessage = "Alignment of the watermark text. Defaults to 'MiddleCenter'.")] + public Enums.Alignments WatermarkAlignment { get; set; } = Enums.Alignments.MiddleCenter; + + [Parameter(Mandatory = false, HelpMessage = "Font name for the watermark text.")] + public string WatermarkFontName { get; set; } = "Arial"; + + [Parameter(Mandatory = false, HelpMessage = "Font size for the watermark text in points. Defaults to 24.")] + public int WatermarkFontSize { get; set; } = 24; + + [Parameter(Mandatory = false, HelpMessage = "Color of the watermark text.")] + public BasicColors WatermarkColor { get; set; } = BasicColors.Gray; + + [Parameter(Mandatory = false, HelpMessage = "Opacity of the watermark (0.0 fully transparent to 1.0 fully opaque). Defaults to 0.3.")] + public double WatermarkOpacity { get; set; } = 0.3; + + [Parameter(Mandatory = false, HelpMessage = "Rotation angle of the watermark text in degrees. Defaults to 0.")] + public float WatermarkRotation { get; set; } = 0; + + // Set chart Size WxH + [Parameter(Mandatory = false, HelpMessage = "Width of the chart in pixels. Defaults to 400.")] + public int Width { get; set; } = 400; + + [Parameter(Mandatory = false, HelpMessage = "Height of the chart in pixels. Defaults to 300.")] + public int Height { get; set; } = 300; + + // Set OutputFolderPath + [Parameter(Mandatory = false, HelpMessage = "Directory path where the chart file will be saved. Defaults to current directory.")] + [ValidatePath()] + public string OutputFolderPath { get; set; } = Directory.GetCurrentDirectory(); + + // Switch to hide values on the chart + [Parameter(Mandatory = false, HelpMessage = "Switch to hide values on the chart.")] + public SwitchParameter HideValues { get; set; } + + protected override void ProcessRecord() + { + Chart.Reset(); + if (Values != null && Labels != null) + { + if (EnableLegend) + { + Chart.EnableLegend = EnableLegend; + // Legend box settings + Chart.LegendOrientation = LegendOrientation; + Chart.LegendAlignment = LegendAlignment; + + // Legend font settings + Chart.LegendFontSize = LegendFontSize; + Chart.LegendFontColor = LegendFontColor; + // Legend border settings + Chart.LegendBorderStyle = LegendBorderStyle; + Chart.LegendBorderSize = LegendBorderSize; + Chart.LegendBorderColor = LegendBorderColor; + } + + if (HideValues) + { + Chart.HideValues = HideValues; + } + + Chart.DonutFraction = DonutFraction; + if (EnableChartBorder) + { + // Chart area settings + Chart.EnableChartBorder = EnableChartBorder; + Chart.ChartBorderColor = ChartBorderColor; + Chart.ChartBorderSize = ChartBorderSize; + Chart.ChartBorderStyle = ChartBorderStyle; + } + // Color palette settings + if (EnableCustomColorPalette) + { + if (CustomColorPalette != null && CustomColorPalette.Length > 0) + { + // Set ScottPlot custom color palette + Chart.EnableCustomColorPalette = EnableCustomColorPalette; + Chart.InvertCustomColorPalette = InvertCustomColorPalette; + Chart.CustomColorPalette = CustomColorPalette; + } + else + { + throw new InvalidOperationException("EnableCustomColorPalette requires CustomColorPalette to be set."); + } + } + else + { + Chart.ColorPalette = ColorPalette; + } + + // this set the area axes margins (Bar Chart) + Chart.AxesMarginsTop = AxesMarginsTop; + Chart.AxesMarginsDown = AxesMarginsDown; + Chart.AxesMarginsLeft = AxesMarginsLeft; + Chart.AxesMarginsRight = AxesMarginsRight; + + // Title settings + if (Title != null) + { + Chart.Title = Title; + Chart.TitleFontBold = TitleFontBold; + Chart.TitleFontSize = TitleFontSize; + Chart.TitleFontColor = TitleFontColor; + } + + // This set the distance of the labels from the chart center (Donut Chart) + Chart.LabelDistance = LabelDistance; + + // Font Settings + Chart.FontName = FontName; + Chart.LabelFontSize = LabelFontSize; + Chart.LabelFontColor = LabelFontColor; + Chart.LabelBold = LabelBold; + + // Background color settings + Chart.FigureBackgroundColor = FigureBackgroundColor; + Chart.DataBackgroundColor = DataBackgroundColor; + + // Watermark settings + Chart.EnableWatermark = EnableWatermark; + Chart.WatermarkText = WatermarkText; + Chart.WatermarkAlignment = WatermarkAlignment; + Chart.WatermarkFontName = WatermarkFontName; + Chart.WatermarkFontSize = WatermarkFontSize; + Chart.WatermarkColor = WatermarkColor; + Chart.WatermarkOpacity = WatermarkOpacity; + Chart.WatermarkRotation = WatermarkRotation; + + // Set file directory save path + Chart.OutputFolderPath = OutputFolderPath; + + Chart.Format = Format; + Donut myDonut = new Donut(); + WriteObject(myDonut.Chart(Values, Labels, Filename, Width, Height)); + } + else + { + WriteObject("Please provide both Values and Labels parameters."); + } + } + } +} \ No newline at end of file diff --git a/Sources/PowerShell/SignalChartPwsh.cs b/Sources/PowerShell/SignalChartPwsh.cs index 78c70bf..2231b5c 100644 --- a/Sources/PowerShell/SignalChartPwsh.cs +++ b/Sources/PowerShell/SignalChartPwsh.cs @@ -170,6 +170,7 @@ public class NewSignalChartCommand : Cmdlet // Set OutputFolderPath [Parameter(Mandatory = false, HelpMessage = "Output folder path where the chart will be saved.")] + [ValidatePath()] public string OutputFolderPath { get; set; } = Directory.GetCurrentDirectory(); protected override void ProcessRecord() diff --git a/Sources/PowerShell/SingleStackedBarChartPwsh.cs b/Sources/PowerShell/SingleStackedBarChartPwsh.cs index fed9e70..04ded83 100644 --- a/Sources/PowerShell/SingleStackedBarChartPwsh.cs +++ b/Sources/PowerShell/SingleStackedBarChartPwsh.cs @@ -167,6 +167,7 @@ public class NewSingleStackedBarChartCommand : Cmdlet // Output folder [Parameter(Mandatory = false, HelpMessage = "Set the output folder path for the chart file.")] + [ValidatePath()] public string OutputFolderPath { get; set; } = Directory.GetCurrentDirectory(); protected override void ProcessRecord() diff --git a/Sources/PowerShell/StackedBarChartPwsh.cs b/Sources/PowerShell/StackedBarChartPwsh.cs index 94c5ca3..0aa39a5 100644 --- a/Sources/PowerShell/StackedBarChartPwsh.cs +++ b/Sources/PowerShell/StackedBarChartPwsh.cs @@ -165,6 +165,7 @@ public class NewStackedBarChartCommand : Cmdlet // Set OutputFolderPath [Parameter(Mandatory = false, HelpMessage = "Set the output folder path for the chart file.")] + [ValidatePath()] public string OutputFolderPath { get; set; } = Directory.GetCurrentDirectory(); protected override void ProcessRecord() From 48d4cb709a8ac75f31f5abbac54363d6074542d2 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 4 Jun 2026 21:18:56 -0400 Subject: [PATCH 03/16] Add tests for New-DonutChart cmdlet: validate execution, output, and error handling --- Tests/AsBuiltReport.Chart.Functions.Tests.ps1 | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 index 4d44cc3..faba0c6 100644 --- a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 +++ b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 @@ -41,6 +41,23 @@ Describe 'AsBuiltReport.Chart Exported Functions' { } } + Context 'New-DonutChart' { + It 'Should run without error with sample input' { + { New-DonutChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + It 'Should return a file path as output' { + $result = New-DonutChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive + $result | Should -BeOfType 'System.IO.FileSystemInfo' + Test-Path $result | Should -BeTrue + } + It 'Should throw error for missing mandatory parameters' { + { New-DonutChart } | Should -Throw + } + It 'Should throw error for mismatched Values and Labels' { + { New-DonutChart -Title 'Test' -Values @(1, 2) -Labels @('A') -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw -ExpectedMessage "Error: Values and labels must be equal." + } + } + Context 'New-BarChart' { It 'Should run without error with sample input' { { New-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw @@ -77,10 +94,10 @@ Describe 'AsBuiltReport.Chart Exported Functions' { { New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A', 'B') -LegendCategories @('X') -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw -ExpectedMessage "Error: Each set of values must have the same length as category names." } It 'Should run without error with a single element (single-bar chart)' { - { New-StackedBarChart -Title 'Test' -Values @(,[double[]]@(1, 2)) -Labels @('A') -LegendCategories @('X', 'Y') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + { New-StackedBarChart -Title 'Test' -Values @(, [double[]]@(1, 2)) -Labels @('A') -LegendCategories @('X', 'Y') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } It 'Should return a file path as output for a single element' { - $result = New-StackedBarChart -Title 'Test' -Values @(,[double[]]@(1, 2)) -Labels @('A') -LegendCategories @('X', 'Y') -Format 'png' -OutputFolderPath $TestDrive + $result = New-StackedBarChart -Title 'Test' -Values @(, [double[]]@(1, 2)) -Labels @('A') -LegendCategories @('X', 'Y') -Format 'png' -OutputFolderPath $TestDrive $result | Should -BeOfType 'System.IO.FileSystemInfo' Test-Path $result | Should -BeTrue } @@ -126,10 +143,10 @@ Describe 'AsBuiltReport.Chart Exported Functions' { Context 'New-SignalChart' { It 'Should run without error with a single signal line' { - { New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } It 'Should return a file path as output' { - $result = New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive + $result = New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive $result | Should -BeOfType 'System.IO.FileSystemInfo' Test-Path $result | Should -BeTrue } @@ -138,15 +155,15 @@ Describe 'AsBuiltReport.Chart Exported Functions' { } It 'Should run without error with DateTime ticks' { $xOffset = (Get-Date '2024-01-01').ToOADate() - { New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3)) -XOffset $xOffset -Period 1.0 -DateTimeTicksBottom -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3)) -XOffset $xOffset -Period 1.0 -DateTimeTicksBottom -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } It 'Should run without error in scatter mode with explicit X values' { - $xValues = @(,[double[]]@(1.0, 2.0, 3.0, 4.0)) - { New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3, 4)) -ScatterXValues $xValues -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + $xValues = @(, [double[]]@(1.0, 2.0, 3.0, 4.0)) + { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3, 4)) -ScatterXValues $xValues -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } It 'Should return a file path as output in scatter mode' { - $xValues = @(,[double[]]@(1.0, 2.0, 3.0, 4.0)) - $result = New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3, 4)) -ScatterXValues $xValues -Format 'png' -OutputFolderPath $TestDrive + $xValues = @(, [double[]]@(1.0, 2.0, 3.0, 4.0)) + $result = New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3, 4)) -ScatterXValues $xValues -Format 'png' -OutputFolderPath $TestDrive $result | Should -BeOfType 'System.IO.FileSystemInfo' Test-Path $result | Should -BeTrue } @@ -154,7 +171,7 @@ Describe 'AsBuiltReport.Chart Exported Functions' { $startDate = (Get-Date '2024-01-01').ToOADate() { $xArr = [double[]]@($startDate, ($startDate + 1.0), ($startDate + 2.0)) - New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3)) -ScatterXValues @(,$xArr) -DateTimeTicksBottom -Format 'png' -OutputFolderPath $TestDrive + New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3)) -ScatterXValues @(, $xArr) -DateTimeTicksBottom -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } It 'Should run without error in scatter mode with multiple lines' { @@ -162,12 +179,12 @@ Describe 'AsBuiltReport.Chart Exported Functions' { { New-SignalChart -Title 'Test' -Values @(@(1, 2, 3), @(4, 5, 6)) -ScatterXValues $xValues -Labels @('Line1', 'Line2') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } It 'Should throw error when ScatterXValues count does not match Values count' { - $xValues = @(,[double[]]@(1.0, 2.0, 3.0)) + $xValues = @(, [double[]]@(1.0, 2.0, 3.0)) { New-SignalChart -Title 'Test' -Values @(@(1, 2, 3), @(4, 5, 6)) -ScatterXValues $xValues -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw -ExpectedMessage "Error: XValues and Values must have the same number of arrays." } It 'Should throw error when XValues elements count does not match Values elements count' { - $xValues = @(,[double[]]@(1.0, 2.0)) - { New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3)) -ScatterXValues $xValues -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw -ExpectedMessage "Error: XValues and Values at index 0 must have the same number of elements." + $xValues = @(, [double[]]@(1.0, 2.0)) + { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3)) -ScatterXValues $xValues -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw -ExpectedMessage "Error: XValues and Values at index 0 must have the same number of elements." } It 'Should throw error for missing mandatory parameters' { { New-SignalChart } | Should -Throw @@ -277,18 +294,18 @@ Describe 'AsBuiltReport.Chart Exported Functions' { Context 'New-SignalChart with watermark' { It 'Should run without error with watermark enabled using defaults' { - { New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark } | Should -Not -Throw + { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark } | Should -Not -Throw } It 'Should return a file when watermark is enabled' { - $result = New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark + $result = New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark $result | Should -BeOfType 'System.IO.FileSystemInfo' Test-Path $result | Should -BeTrue } It 'Should run without error with custom watermark color and opacity' { - { New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark -WatermarkColor Green -WatermarkOpacity 0.4 } | Should -Not -Throw + { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark -WatermarkColor Green -WatermarkOpacity 0.4 } | Should -Not -Throw } It 'Should run without error without watermark (disabled by default)' { - { New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } } } @@ -332,8 +349,8 @@ Describe 'AsBuiltReport.Chart Exported Functions' { { New-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } It 'EnableWatermark should not persist from one New-SignalChart call to the next' { - New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3)) -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark | Out-Null - { New-SignalChart -Title 'Test' -Values @(,[double[]]@(1, 2, 3)) -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3)) -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark | Out-Null + { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3)) -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } } } From be8d602ec06425688502cd5471109f78985191e4 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 6 Jun 2026 12:53:13 -0400 Subject: [PATCH 04/16] Update CHANGELOG.md for version 0.3.3: enhance documentation for New-DonutChart cmdlet and update dependencies --- CHANGELOG.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb4b8bc..0257d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,20 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add New-DonutChart cmdlet and update module exports +- Add support for Donut from Slices chart: [Donut from Slices](https://scottplot.net/cookbook/5/Pie/PieDonut/) - Add pester test to validate the functionality of the New-DonutChart cmdlet +- Add example 12/13 to document on how to use the New-DonutChart cmdlet ### Changed -- Update module version to 0.3.3 +- Update module v0.3.3 +- Update SkiaSharp .NET dependency to v3.119.4 +- Update HarfBuzzSharp .NET dependency to v8.3.1.5 ## [0.3.2] - 2026-05-05 +### Added + +- Add New-SingleStackedBarChart cmdlet and update module exports +- Add pester test to validate the functionality of the New-SingleStackedBarChart cmdlet + ### Changed -- Update module v0.3.3 -- Update SkiaSharp .NET dependency to v3.119.4 -- Update HarfBuzzSharp .NET dependency to v8.3.1.5 +- Update module version to 0.3.2 ## [0.3.1] - 2026-04-23 From a25bb5715c55e25b65115b02d380f12f95917019 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 6 Jun 2026 12:54:39 -0400 Subject: [PATCH 05/16] Add radar chart support to Todo.md --- Todo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Todo.md b/Todo.md index 8b13789..ab6844c 100644 --- a/Todo.md +++ b/Todo.md @@ -1 +1 @@ - +- Add radar chart support: [Radar Chart](https://scottplot.net/cookbook/5/Radar/) \ No newline at end of file From 7b16f554726d25e92f40e7dd22b704160889564c Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 10 Jun 2026 22:12:53 -0400 Subject: [PATCH 06/16] Add radar chart support: implement New-RadarChart cmdlet and associated classes --- AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 | 2 +- CHANGELOG.md | 5 +- Sources/Chart.cs | 21 ++ Sources/PowerShell/RadarChartPwsh.cs | 269 +++++++++++++++++++ Sources/RadarChart.cs | 251 +++++++++++++++++ Todo.md | 3 +- 6 files changed, 547 insertions(+), 4 deletions(-) create mode 100644 Sources/PowerShell/RadarChartPwsh.cs create mode 100644 Sources/RadarChart.cs diff --git a/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 b/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 index 9ab6ce1..4fa00be 100644 --- a/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 +++ b/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 @@ -69,7 +69,7 @@ # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = 'New-PieChart', 'New-BarChart', 'New-StackedBarChart', 'New-SignalChart', 'New-SingleStackedBarChart', 'New-DonutChart' + FunctionsToExport = 'New-PieChart', 'New-BarChart', 'New-StackedBarChart', 'New-SignalChart', 'New-SingleStackedBarChart', 'New-DonutChart', 'New-RadarChart' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. # CmdletsToExport = '*' diff --git a/CHANGELOG.md b/CHANGELOG.md index 0257d6c..ddc3341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add support for Donut from Slices chart: [Donut from Slices](https://scottplot.net/cookbook/5/Pie/PieDonut/) -- Add pester test to validate the functionality of the New-DonutChart cmdlet -- Add example 12/13 to document on how to use the New-DonutChart cmdlet + - Add pester test to validate the functionality of the New-DonutChart cmdlet + - Add example 12/13 to document on how to use the New-DonutChart cmdlet +- Add radar chart support: implement New-RadarChart cmdlet and associated classes ### Changed diff --git a/Sources/Chart.cs b/Sources/Chart.cs index cb260b1..fc574c4 100644 --- a/Sources/Chart.cs +++ b/Sources/Chart.cs @@ -50,6 +50,24 @@ public static double LabelDistance } } + // this set the distance of the labels from the chart center (Pie Chart) + internal static double _spokesLength; + public static double SpokesLength + { + get { return _spokesLength; } + set + { + if (value >= 5 && value <= 50) + { + _spokesLength = value; + } + else + { + throw new ArgumentException("Error: SpokesLength value range must be from 5 to 50."); + } + } + } + // this set the orientation chart area (Bar Chart) public static Orientations AreaOrientation { get; set; } = Orientations.Vertical; @@ -71,6 +89,9 @@ public static double AreaExplodeFraction } } + // Center text for Donut Chart + public static string DonutCenterText { get; set; } + // this set the distance of the chart area elements (Donut Chart) internal static double _donutFraction; public static double DonutFraction diff --git a/Sources/PowerShell/RadarChartPwsh.cs b/Sources/PowerShell/RadarChartPwsh.cs new file mode 100644 index 0000000..a9a362e --- /dev/null +++ b/Sources/PowerShell/RadarChartPwsh.cs @@ -0,0 +1,269 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Management.Automation; + +namespace AsBuiltReportChart.PowerShell +{ + [Cmdlet(VerbsCommon.New, "RadarChart")] + public class NewRadarChartCommand : Cmdlet + { + // Declare the parameters for the cmdlet. + [Parameter(Mandatory = false, HelpMessage = "Output filename for the chart. Defaults to a randomly generated 8-character token.")] + public string Filename { get; set; } = Chart.GenerateToken(8); + + [Parameter(Mandatory = true, HelpMessage = "List of arrays containing numeric values for each radar series.")] + public List Values { get; set; } + + [Parameter(Mandatory = true, HelpMessage = "Array of labels corresponding to each radar series.")] + public string[] LegendLabels { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Array of labels corresponding to each radar series.")] + public string[] SpokeLabels { get; set; } + + [Parameter(Mandatory = true, HelpMessage = "Title text to display on the chart.")] + public string Title { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Switch to make the title font bold.")] + public SwitchParameter TitleFontBold { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Font size for the title. Defaults to 14.")] + public int TitleFontSize { get; set; } = 14; + + [Parameter(Mandatory = false, HelpMessage = "Font color for the title.")] + public BasicColors TitleFontColor { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Switch to enable the legend on the chart.")] + public SwitchParameter EnableLegend { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Orientation of the legend (Vertical or Horizontal). Defaults to Vertical.")] + public Enums.Orientations LegendOrientation { get; set; } = Enums.Orientations.Vertical; + + [Parameter(Mandatory = false, HelpMessage = "Alignment of the legend on the chart. Defaults to UpperRight.")] + public Enums.Alignments LegendAlignment { get; set; } = Enums.Alignments.UpperRight; + + [Parameter(Mandatory = true, HelpMessage = "Output format for the chart (e.g., PNG, JPG, SVG).")] + public Formats Format { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Color of the chart border.")] + public BasicColors ChartBorderColor { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Size of the chart border in pixels. Defaults to 1.")] + public int ChartBorderSize { get; set; } = 1; + + [Parameter(Mandatory = false, HelpMessage = "Style of the chart border.")] + public Enums.BorderStyles ChartBorderStyle { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Switch to enable the chart border.")] + public SwitchParameter EnableChartBorder { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Font size for the legend. Defaults to 14.")] + public int LegendFontSize { get; set; } = 14; + + [Parameter(Mandatory = false, HelpMessage = "Font color for the legend. Defaults to Black.")] + public BasicColors LegendFontColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Border style for the legend.")] + public Enums.BorderStyles LegendBorderStyle { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Border size for the legend in pixels. Defaults to 1.")] + public int LegendBorderSize { get; set; } = 1; + + [Parameter(Mandatory = false, HelpMessage = "Border color for the legend. Defaults to Black.")] + public BasicColors LegendBorderColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Color palette for the chart. Defaults to Category10.")] + public Enums.ColorPalettes ColorPalette { get; set; } = Enums.ColorPalettes.Category10; + + [Parameter(Mandatory = false, HelpMessage = "Switch to enable custom color palette.")] + public SwitchParameter EnableCustomColorPalette { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Invert the custom color palette.")] + public SwitchParameter InvertCustomColorPalette { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Array of custom hex color values for the chart.")] + public string[] CustomColorPalette { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Font name to use for all text. Defaults to Arial.")] + public string FontName { get; set; } = "Arial"; + + // Label Font settings + [Parameter(Mandatory = false, HelpMessage = "Font size for chart labels. Defaults to 14.")] + public int LabelFontSize { get; set; } = 14; + + [Parameter(Mandatory = false, HelpMessage = "Font color for labels. Defaults to Black.")] + public BasicColors LabelFontColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Switch to make label font bold.")] + public SwitchParameter LabelBold { get; set; } + + // this set the distance of the labels from the chart center (Radar Chart) + [Parameter(Mandatory = false, HelpMessage = "Distance of labels from the chart center (10 to 50). Defaults to 10.")] + public double SpokesLength { get; set; } = 10; + + // this set the area axes margins (Bar Chart) + [Parameter(Mandatory = false, HelpMessage = "Top margin for the chart area. Defaults to 0.2.")] + public double AxesMarginsTop { get; set; } = 0.2; + + [Parameter(Mandatory = false, HelpMessage = "Bottom margin for the chart area. Defaults to 0.05.")] + public double AxesMarginsDown { get; set; } = 0.05; + + [Parameter(Mandatory = false, HelpMessage = "Left margin for the chart area. Defaults to 0.05.")] + public double AxesMarginsLeft { get; set; } = 0.05; + + [Parameter(Mandatory = false, HelpMessage = "Right margin for the chart area. Defaults to 0.05.")] + public double AxesMarginsRight { get; set; } = 0.05; + + [Parameter(Mandatory = false, HelpMessage = "Background color of the entire figure (canvas).")] + public BasicColors? FigureBackgroundColor { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Background color of the data area inside the axes.")] + public BasicColors? DataBackgroundColor { get; set; } + + // Watermark settings + [Parameter(Mandatory = false, HelpMessage = "Enable a watermark on the chart.")] + public SwitchParameter EnableWatermark { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Text to display as the watermark. Defaults to 'Confidential'.")] + public string WatermarkText { get; set; } = "Confidential"; + + [Parameter(Mandatory = false, HelpMessage = "Alignment of the watermark text. Defaults to 'MiddleCenter'.")] + public Enums.Alignments WatermarkAlignment { get; set; } = Enums.Alignments.MiddleCenter; + + [Parameter(Mandatory = false, HelpMessage = "Font name for the watermark text.")] + public string WatermarkFontName { get; set; } = "Arial"; + + [Parameter(Mandatory = false, HelpMessage = "Font size for the watermark text in points. Defaults to 24.")] + public int WatermarkFontSize { get; set; } = 24; + + [Parameter(Mandatory = false, HelpMessage = "Color of the watermark text.")] + public BasicColors WatermarkColor { get; set; } = BasicColors.Gray; + + [Parameter(Mandatory = false, HelpMessage = "Opacity of the watermark (0.0 fully transparent to 1.0 fully opaque). Defaults to 0.3.")] + public double WatermarkOpacity { get; set; } = 0.3; + + [Parameter(Mandatory = false, HelpMessage = "Rotation angle of the watermark text in degrees. Defaults to 0.")] + public float WatermarkRotation { get; set; } = 0; + + // Set chart Size WxH + [Parameter(Mandatory = false, HelpMessage = "Width of the chart in pixels. Defaults to 400.")] + public int Width { get; set; } = 400; + + [Parameter(Mandatory = false, HelpMessage = "Height of the chart in pixels. Defaults to 300.")] + public int Height { get; set; } = 300; + + // Set OutputFolderPath + [Parameter(Mandatory = false, HelpMessage = "Directory path where the chart file will be saved. Defaults to current directory.")] + [ValidatePath()] + public string OutputFolderPath { get; set; } = Directory.GetCurrentDirectory(); + + protected override void ProcessRecord() + { + Chart.Reset(); + if (Values != null && LegendLabels != null) + { + if (EnableLegend) + { + Chart.EnableLegend = EnableLegend; + // Legend box settings + Chart.LegendOrientation = LegendOrientation; + Chart.LegendAlignment = LegendAlignment; + + // Legend font settings + Chart.LegendFontSize = LegendFontSize; + Chart.LegendFontColor = LegendFontColor; + // Legend border settings + Chart.LegendBorderStyle = LegendBorderStyle; + Chart.LegendBorderSize = LegendBorderSize; + Chart.LegendBorderColor = LegendBorderColor; + } + + if (EnableChartBorder) + { + // Chart area settings + Chart.EnableChartBorder = EnableChartBorder; + Chart.ChartBorderColor = ChartBorderColor; + Chart.ChartBorderSize = ChartBorderSize; + Chart.ChartBorderStyle = ChartBorderStyle; + } + // Color palette settings + if (EnableCustomColorPalette) + { + if (CustomColorPalette != null && CustomColorPalette.Length > 0) + { + // Set ScottPlot custom color palette + Chart.EnableCustomColorPalette = EnableCustomColorPalette; + Chart.InvertCustomColorPalette = InvertCustomColorPalette; + Chart.CustomColorPalette = CustomColorPalette; + } + else + { + throw new InvalidOperationException("EnableCustomColorPalette requires CustomColorPalette to be set."); + } + } + else + { + Chart.ColorPalette = ColorPalette; + } + + // this set the area axes margins (Bar Chart) + Chart.AxesMarginsTop = AxesMarginsTop; + Chart.AxesMarginsDown = AxesMarginsDown; + Chart.AxesMarginsLeft = AxesMarginsLeft; + Chart.AxesMarginsRight = AxesMarginsRight; + + // Title settings + if (Title != null) + { + Chart.Title = Title; + Chart.TitleFontBold = TitleFontBold; + Chart.TitleFontSize = TitleFontSize; + Chart.TitleFontColor = TitleFontColor; + } + + // This set the distance of the labels from the chart center (Radar Chart) + Chart.SpokesLength = SpokesLength; + + // Font Settings + Chart.FontName = FontName; + Chart.LabelFontSize = LabelFontSize; + Chart.LabelFontColor = LabelFontColor; + Chart.LabelBold = LabelBold; + + // Background color settings + Chart.FigureBackgroundColor = FigureBackgroundColor; + Chart.DataBackgroundColor = DataBackgroundColor; + + // Watermark settings + Chart.EnableWatermark = EnableWatermark; + Chart.WatermarkText = WatermarkText; + Chart.WatermarkAlignment = WatermarkAlignment; + Chart.WatermarkFontName = WatermarkFontName; + Chart.WatermarkFontSize = WatermarkFontSize; + Chart.WatermarkColor = WatermarkColor; + Chart.WatermarkOpacity = WatermarkOpacity; + Chart.WatermarkRotation = WatermarkRotation; + + // Set file directory save path + Chart.OutputFolderPath = OutputFolderPath; + + Chart.Format = Format; + + Radar myRadar = new Radar(); + + if (SpokeLabels != null && SpokeLabels.Length > 0) + { + WriteObject(myRadar.Chart(Values, LegendLabels, SpokeLabels, Filename, Width, Height)); + } + else + { + WriteObject(myRadar.Chart(Values, LegendLabels, Filename, Width, Height)); + } + } + else + { + WriteObject("Please provide both Values and LegendLabels parameters."); + } + } + } +} \ No newline at end of file diff --git a/Sources/RadarChart.cs b/Sources/RadarChart.cs new file mode 100644 index 0000000..86c6af1 --- /dev/null +++ b/Sources/RadarChart.cs @@ -0,0 +1,251 @@ +using ScottPlot; +using System; +using System.IO; +using System.Collections.Generic; +namespace AsBuiltReportChart +{ + internal class Radar : Chart + { + public Radar() { } + public object Chart(List values, string[] labels, string filename = "output", int width = 400, int height = 300) + { + if (values.Count == labels.Length) + { + Plot myPlot = new Plot(); + + if (EnableCustomColorPalette) + { + if (_customColorPalette != null && _customColorPalette.Length > 0) + { + myPlot.Add.Palette = colorPalette = new ScottPlot.Palettes.Custom(_customColorPalette); + } + else + { + throw new InvalidOperationException("CustomColorPalette is empty. Please provide valid color values."); + } + } + else + { + // Set ScottPlot native color palette + if (colorPalette != null) + { + myPlot.Add.Palette = colorPalette; + } + } + + var radar = myPlot.Add.Radar(values); + + // set each slice value to its label + if (EnableLegend) + { + for (var i = 0; i < radar.Series.Count; i++) + { + radar.Series[i].LegendText = labels[i]; + } + } + + // hide unnecessary plot components + myPlot.Axes.Frameless(); + myPlot.HideGrid(); + + if (EnableLegend) + { + // Legend Font Properties + myPlot.Legend.FontName = FontName; + myPlot.Legend.FontSize = LegendFontSize; + myPlot.Legend.FontColor = GetDrawingColor(LegendFontColor); + + // Legend box Style Properties + myPlot.Legend.OutlineColor = GetDrawingColor(LegendBorderColor); + myPlot.Legend.OutlineWidth = LegendBorderSize; + + myPlot.Legend.OutlinePattern = LegendBorderStyleMap[LegendBorderStyle]; + + myPlot.Legend.Orientation = LegendOrientationMap[LegendOrientation]; + + myPlot.Legend.Alignment = LegendAlignmentMap[LegendAlignment]; + } + + if (EnableChartBorder) + { + myPlot.FigureBorder = new LineStyle() + { + Color = GetDrawingColor(ChartBorderColor), + Width = ChartBorderSize, + Pattern = ChartBorderStyleMap[ChartBorderStyle], + }; + } + + // Set title properties + if (Title != null) + { + myPlot.Title(Title); + myPlot.Axes.Title.Label.FontSize = TitleFontSize; + myPlot.Axes.Title.Label.ForeColor = GetDrawingColor(TitleFontColor); + myPlot.Axes.Title.Label.Bold = TitleFontBold; + myPlot.Axes.Title.Label.FontName = FontName; + } + + // Set margins settings + myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); + + // Set background colors + if (FigureBackgroundColor.HasValue) + { + myPlot.FigureBackground.Color = GetDrawingColor(FigureBackgroundColor.Value); + } + if (DataBackgroundColor.HasValue) + { + myPlot.DataBackground.Color = GetDrawingColor(DataBackgroundColor.Value); + } + + // Set the distance of the chart area elements (Radar Chart) + // radar.SliceLabelDistance = _labelDistance; + + // Apply watermark if enabled + ApplyWatermark(myPlot); + + // Set filetpath to save + string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); + + // Set filename + return SaveInFormat(myPlot, width, height, Filepath, filename, Format); + } + else + { + throw new ArgumentException("Error: Values and labels must be equal."); + } + } + public object Chart(List values, string[] labels, string[] categoryNames, string filename = "output", int width = 400, int height = 300) + { + if (values.Count == labels.Length) + { + Plot myPlot = new Plot(); + + if (EnableCustomColorPalette) + { + if (_customColorPalette != null && _customColorPalette.Length > 0) + { + myPlot.Add.Palette = colorPalette = new ScottPlot.Palettes.Custom(_customColorPalette); + } + else + { + throw new InvalidOperationException("CustomColorPalette is empty. Please provide valid color values."); + } + } + else + { + // Set ScottPlot native color palette + if (colorPalette != null) + { + myPlot.Add.Palette = colorPalette; + } + } + + var radar = myPlot.Add.Radar(values); + + // set each slice value to its label + if (EnableLegend) + { + for (var i = 0; i < radar.Series.Count; i++) + { + radar.Series[i].LegendText = labels[i]; + } + } + + if (categoryNames != null) + { + if (categoryNames.Length > 0 && categoryNames.Length == values[0].Length) + { + radar.PolarAxis.SetSpokes(categoryNames, length: SpokesLength); + // set each PolarAxis value to its label + for (var i = 0; i < radar.PolarAxis.Spokes.Count; i++) + { + radar.PolarAxis.Spokes[i].LabelStyle.FontSize = LabelFontSize; + radar.PolarAxis.Spokes[i].LabelStyle.ForeColor = GetDrawingColor(LabelFontColor); + radar.PolarAxis.Spokes[i].LabelStyle.Bold = LabelBold; + radar.PolarAxis.Spokes[i].LabelStyle.FontName = FontName; + } + } + else + { + throw new ArgumentException("Error: SpokeLabels must be provided and its length must match the number of values in each series."); + } + } + + // hide unnecessary plot components + myPlot.Axes.Frameless(); + myPlot.HideGrid(); + + if (EnableLegend) + { + // Legend Font Properties + myPlot.Legend.FontName = FontName; + myPlot.Legend.FontSize = LegendFontSize; + myPlot.Legend.FontColor = GetDrawingColor(LegendFontColor); + + // Legend box Style Properties + myPlot.Legend.OutlineColor = GetDrawingColor(LegendBorderColor); + myPlot.Legend.OutlineWidth = LegendBorderSize; + + myPlot.Legend.OutlinePattern = LegendBorderStyleMap[LegendBorderStyle]; + + myPlot.Legend.Orientation = LegendOrientationMap[LegendOrientation]; + + myPlot.Legend.Alignment = LegendAlignmentMap[LegendAlignment]; + } + + if (EnableChartBorder) + { + myPlot.FigureBorder = new LineStyle() + { + Color = GetDrawingColor(ChartBorderColor), + Width = ChartBorderSize, + Pattern = ChartBorderStyleMap[ChartBorderStyle], + }; + } + + // Set title properties + if (Title != null) + { + myPlot.Title(Title); + myPlot.Axes.Title.Label.FontSize = TitleFontSize; + myPlot.Axes.Title.Label.ForeColor = GetDrawingColor(TitleFontColor); + myPlot.Axes.Title.Label.Bold = TitleFontBold; + myPlot.Axes.Title.Label.FontName = FontName; + } + + // Set margins settings + myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); + + // Set background colors + if (FigureBackgroundColor.HasValue) + { + myPlot.FigureBackground.Color = GetDrawingColor(FigureBackgroundColor.Value); + } + if (DataBackgroundColor.HasValue) + { + myPlot.DataBackground.Color = GetDrawingColor(DataBackgroundColor.Value); + } + + // Set the distance of the chart area elements (Radar Chart) + // radar.SliceLabelDistance = _labelDistance; + + // Apply watermark if enabled + ApplyWatermark(myPlot); + + // Set filetpath to save + string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); + + // Set filename + return SaveInFormat(myPlot, width, height, Filepath, filename, Format); + } + else + { + throw new ArgumentException("Error: Values and labels must be equal."); + } + } + } +} + + diff --git a/Todo.md b/Todo.md index ab6844c..d6cc011 100644 --- a/Todo.md +++ b/Todo.md @@ -1 +1,2 @@ -- Add radar chart support: [Radar Chart](https://scottplot.net/cookbook/5/Radar/) \ No newline at end of file +[Done] Add radar chart support: [Radar Chart](https://scottplot.net/cookbook/5/Radar/) +[] Calculate the spoke length based on the maximum value in the dataset to ensure the radar chart is properly scaled. \ No newline at end of file From c2fa862c22dd6a401c6187c667c072ea92c06667 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 11 Jun 2026 10:23:59 -0400 Subject: [PATCH 07/16] Update README with new chart examples and details --- README.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 89ab0de..3cbcfd0 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,13 @@ New-PieChart -Title 'Test' -Values @(1,2) -Labels @('A','B') -Format 'png' -Enab ``` ![PieChart](./Samples/PieChart.png) +### Donut Chart +```powershell +# Generate a Donut Chart with the title 'Test', values of 1 and 2, labels 'A' and 'B', and export the chart in PNG format. Enable the legend and set the width to 600 pixels, height to 400 pixels, title font size to 20, and label font size to 16. +New-DonutChart -Title 'Test' -Values @(1,2) -Labels @('A','B') -Format 'png' -EnableLegend -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 +``` +![DonutChart](./Samples/DonutChart.png) + ### Bar Chart ```powershell # Generate a Bar Chart with the title 'Test', values of 1 and 2, labels 'A' and 'B', and export the chart in PNG format. Enable the legend and set the width to 600 pixels, height to 400 pixels, title font size to 20, and label font size to 16. @@ -104,6 +111,20 @@ New-StackedBarChart -Title 'Test' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -L ``` ![StackedBarChart](./Samples/StackedBarChart.png) +### Single Stacked Bar Chart +```powershell +# Generate a Single Stacked Bar Chart with the title 'Test', values @(18, 15, 10, 5, 8) for bar 'A' and @(18, 15, 10, 5, 8) for bar 'B' (one inner -Values array per bar matching -Labels), legend categories 'Value1' and 'Value2' for the stacked segments, and export the chart in PNG format. Enable the legend, set the legend orientation to horizontal, align the legend to the upper center, set the width to 600 pixels, height to 400 pixels, title font size to 20, label font size to 16, and axes margins top to 1. +New-SingleStackedBarChart -Title 'Test' -Values @(18, 15, 10, 5, 8) -Label 'Workload Type' -LegendCategories @('Category 1', 'Category 2', 'Category 3', 'Category 4', 'Category 5') -Width 600 -Height 200 -Format png -AreaOrientation Horizontal -EnableLegend -LegendOrientation Horizontal -LegendAlignment UpperCenter -TitleFontBold -TitleFontSize 20 +``` +![SingleStackedBarChart](./Samples/SingleStackedBarChart.png) + +### Radar Chart +```powershell +# Generate a Radar Chart with the title 'Test', values of 1 and 2, labels 'A' and 'B', and export the chart in PNG format. Enable the legend and set the width to 600 pixels, height to 400 pixels, title font size to 20, and label font size to 16. +New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8),@(3,5,4,2)) -LegendLabels @('A', 'B') -Format 'png' -EnableLegend -LegendOrientation Horizontal -LegendAlignment UpperCenter -Filename RadarChart -Width 600 -Height 400 -SpokeLabels @("Wins", "Poles", "Podiums", "Points") -TitleFontSize 20 +``` +![RadarChart](./Samples/RadarChart.png) + ### :blue_book: Example Index All examples in the latest release of AsBuiltReport.Chart can be found in the table below. @@ -157,9 +178,3 @@ New-SignalChart -Title 'Throughput' -Values @(,[double[]]@(1,2,3,4,5)) -Format ' ## :x: Known Issues - No known issues at this time. - - - - - - From 4cecd6884e36d47ab24c5de197c45a8886d78754 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 11 Jun 2026 10:25:02 -0400 Subject: [PATCH 08/16] Add files via upload --- Samples/DonutChart.png | Bin 0 -> 14918 bytes Samples/RadarChart.png | Bin 0 -> 24013 bytes Samples/SingleStackedBarChart.png | Bin 0 -> 20255 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Samples/DonutChart.png create mode 100644 Samples/RadarChart.png create mode 100644 Samples/SingleStackedBarChart.png diff --git a/Samples/DonutChart.png b/Samples/DonutChart.png new file mode 100644 index 0000000000000000000000000000000000000000..c663c0b217bb6316fb7913ba1aabfd61c0a4862d GIT binary patch literal 14918 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 AT>t<8 literal 0 HcmV?d00001 diff --git a/Samples/RadarChart.png b/Samples/RadarChart.png new file mode 100644 index 0000000000000000000000000000000000000000..dfaed97c248d3a4f5f8b6c275aa01455bdb889d5 GIT binary patch literal 24013 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

7(M1 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 Aga7~l literal 0 HcmV?d00001 diff --git a/Samples/SingleStackedBarChart.png b/Samples/SingleStackedBarChart.png new file mode 100644 index 0000000000000000000000000000000000000000..0524892e2f8b8c7bc89ed4fd96c98b8ae9fcac5b GIT binary patch literal 20255 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 Date: Thu, 11 Jun 2026 20:27:01 -0400 Subject: [PATCH 09/16] Revise README with new links and examples Updated README.md to include new links, examples, and watermark support details. --- README.md | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3cbcfd0..cab156d 100644 --- a/README.md +++ b/README.md @@ -129,17 +129,22 @@ New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8),@(3,5,4,2)) -LegendLabels @ All examples in the latest release of AsBuiltReport.Chart can be found in the table below. -| Name | Description | -| ------------------------------------ | ---------------------------------------------------- | -| [Example1](./Examples/Example01.ps1) | Basic Pie Chart | -| [Example2](./Examples/Example02.ps1) | Pie Chart with Legend, Custom Colors and Border | -| [Example3](./Examples/Example03.ps1) | Basic Bar Chart | -| [Example4](./Examples/Example04.ps1) | Bar Chart with Advanced Options | -| [Example5](./Examples/Example05.ps1) | Basic Stacked Bar Chart | -| [Example6](./Examples/Example06.ps1) | Stacked Bar Chart with Advanced Options | -| [Example7](./Examples/Example07.ps1) | Basic Signal Chart (Line Chart) | -| [Example8](./Examples/Example08.ps1) | Signal Chart with DateTime X-Axis (Time-Series Data) | -| [Example9](./Examples/Example09.ps1) | Signal Chart with Multiple Lines | +| Name | Description | +| ------------------------------------- | ---------------------------------------------------- | +| [Example1](./Examples/Example01.ps1) | Basic Pie Chart | +| [Example2](./Examples/Example02.ps1) | Pie Chart with Legend, Custom Colors and Border | +| [Example3](./Examples/Example03.ps1) | Basic Bar Chart | +| [Example4](./Examples/Example04.ps1) | Bar Chart with Advanced Options | +| [Example5](./Examples/Example05.ps1) | Basic Stacked Bar Chart | +| [Example6](./Examples/Example06.ps1) | Stacked Bar Chart with Advanced Options | +| [Example7](./Examples/Example07.ps1) | Basic Signal Chart (Line Chart) | +| [Example8](./Examples/Example08.ps1) | Signal Chart with DateTime X-Axis (Time-Series Data) | +| [Example9](./Examples/Example09.ps1) | Signal Chart with Multiple Lines | +| [Example10](./Examples/Example10.ps1) | Basic Single Stacked Bar Chart | +| [Example11](./Examples/Example11.ps1) | Single Stacked Bar Chart with Advanced Options | +| [Example12](./Examples/Example12.ps1) | Basic Donut Chart | +| [Example13](./Examples/Example13.ps1) | Donut Chart with Legend, Custom Colors and Border | + ## Watermark Support @@ -147,14 +152,14 @@ All chart types support an optional watermark that overlays semi-transparent tex ### Watermark Parameters -| Parameter | Type | Default | Description | -|---|---|---|---| -| `EnableWatermark` | Switch | (off) | Enables the watermark overlay. | -| `WatermarkText` | String | `Confidential` | Text to display as the watermark. | -| `WatermarkFontName` | String | `Arial` | Font family for the watermark text. | -| `WatermarkFontSize` | Int | `24` | Font size (points) for the watermark text. | -| `WatermarkColor` | BasicColors | `Gray` | Color of the watermark text. | -| `WatermarkOpacity` | Double | `0.3` | Opacity (0.0–1.0) of the watermark. Lower values are more transparent. | +| Parameter | Type | Default | Description | +| ------------------- | ----------- | -------------- | ---------------------------------------------------------------------- | +| `EnableWatermark` | Switch | (off) | Enables the watermark overlay. | +| `WatermarkText` | String | `Confidential` | Text to display as the watermark. | +| `WatermarkFontName` | String | `Arial` | Font family for the watermark text. | +| `WatermarkFontSize` | Int | `24` | Font size (points) for the watermark text. | +| `WatermarkColor` | BasicColors | `Gray` | Color of the watermark text. | +| `WatermarkOpacity` | Double | `0.3` | Opacity (0.0–1.0) of the watermark. Lower values are more transparent. | ### Watermark Examples From 92006da78e44c03b284a6eeeab0119b4332c8c56 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 11 Jun 2026 20:29:22 -0400 Subject: [PATCH 10/16] Update README.md with new content and examples --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cab156d..bbb6f6b 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,8 @@ All examples in the latest release of AsBuiltReport.Chart can be found in the ta | [Example11](./Examples/Example11.ps1) | Single Stacked Bar Chart with Advanced Options | | [Example12](./Examples/Example12.ps1) | Basic Donut Chart | | [Example13](./Examples/Example13.ps1) | Donut Chart with Legend, Custom Colors and Border | - +| [Example14](./Examples/Example14.ps1) | Basic Radar Chart with Legend | +| [Example15](./Examples/Example15.ps1) | Radar Chart with Legend, Custom Colors and Border | ## Watermark Support From 32614ce77e5fa71ca35db7c21bc069f891d3ec25 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 11 Jun 2026 20:32:24 -0400 Subject: [PATCH 11/16] Update examples and enhance Radar and Donut chart functionalities - Renamed example files to reflect correct numbering and descriptions. - Added Example 14: Basic Radar Chart and Example 15: Advanced Radar Chart. - Introduced LabelDistance parameter for better label positioning in Radar charts. - Enhanced Donut chart to support center text annotation. --- CHANGELOG.md | 2 + Examples/Example12.ps1 | 2 +- Examples/Example13.ps1 | 2 +- Examples/Example14.ps1 | 62 +++++++++++++++++++++++++ Examples/Example15.ps1 | 67 ++++++++++++++++++++++++++++ Sources/DonutChart.cs | 14 ++++-- Sources/PowerShell/RadarChartPwsh.cs | 10 ++++- Sources/RadarChart.cs | 1 + 8 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 Examples/Example14.ps1 create mode 100644 Examples/Example15.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index ddc3341..dc64ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add pester test to validate the functionality of the New-DonutChart cmdlet - Add example 12/13 to document on how to use the New-DonutChart cmdlet - Add radar chart support: implement New-RadarChart cmdlet and associated classes + - Add pester test to validate the functionality of the New-RadarChart cmdlet + - Add example 14/15 to document on how to use the New-RadarChart cmdlet ### Changed diff --git a/Examples/Example12.ps1 b/Examples/Example12.ps1 index 25b5a1f..8da6115 100644 --- a/Examples/Example12.ps1 +++ b/Examples/Example12.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Example 01 - Basic Donut Chart + Example 12 - Basic Donut Chart .DESCRIPTION This example demonstrates how to create a basic Donut Chart using the AsBuiltReport.Chart module. diff --git a/Examples/Example13.ps1 b/Examples/Example13.ps1 index 908cc57..3bf3186 100644 --- a/Examples/Example13.ps1 +++ b/Examples/Example13.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Example 02 - Donut Chart with Legend, Custom Colors and Border + Example 13 - Donut Chart with Legend, Custom Colors and Border .DESCRIPTION This example demonstrates how to create a Donut Chart with additional visual options, including: diff --git a/Examples/Example14.ps1 b/Examples/Example14.ps1 new file mode 100644 index 0000000..37845ab --- /dev/null +++ b/Examples/Example14.ps1 @@ -0,0 +1,62 @@ +<# + .SYNOPSIS + Example 14 - Basic Radar Chart + + .DESCRIPTION + This example demonstrates how to create a basic Radar Chart using the AsBuiltReport.Chart module. + The chart displays a simple security posture assessment for two data centers across multiple categories. +#> + +[CmdletBinding()] +param ( + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, + [string] $Format = 'png' +) + +<# + Starting with PowerShell v3, modules are auto-imported when needed. Importing the module here + ensures clarity and avoids ambiguity. +#> + +# Import-Module AsBuiltReport.Chart -Force -Verbose:$false + +<# + Since the chart output is a file, specify the output folder path using $OutputFolderPath. +#> + +$OutputFolderPath = Resolve-Path $Path + +<# + Define the data to be displayed in the chart. + In a real-world scenario these values would come from your infrastructure query. +#> + +$ChartTitle = 'Security Posture Assessment' +$Values = @(@(3,5,4,2)) +$Labels = @('USA DataCenter') + +<# + The New-RadarChart cmdlet generates the Radar Chart image. + + -Title : Sets the chart title displayed at the top of the image. + -Values : Array of numeric values, one per axis. + -LegendLabels : Array of label strings corresponding to each value. + -Format : Output file format (e.g. png, jpg, svg). + -OutputFolderPath : Directory where the generated chart file will be saved. + -Width : Width of the chart image in pixels. + -Height : Height of the chart image in pixels. + -ColorPalette : Predefined color palette (e.g. Category20, Pastel). + -Filename : Name of the output file (without extension). +#> + +New-RadarChart ` + -Title $ChartTitle ` + -Values $Values ` + -LegendLabels $Labels ` + -Format $Format ` + -EnableLegend ` + -OutputFolderPath $OutputFolderPath ` + -Width 600 ` + -Height 400 ` + -ColorPalette Category20 ` + -Filename 'Example14-RadarChart' \ No newline at end of file diff --git a/Examples/Example15.ps1 b/Examples/Example15.ps1 new file mode 100644 index 0000000..d1ad568 --- /dev/null +++ b/Examples/Example15.ps1 @@ -0,0 +1,67 @@ +<# + .SYNOPSIS + Example 15 - Advanced Radar Chart + + .DESCRIPTION + This example demonstrates how to create a Advanced Radar Chart using the AsBuiltReport.Chart module. + The chart displays a security posture assessment for two data centers across multiple categories. +#> + +[CmdletBinding()] +param ( + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, + [string] $Format = 'png' +) + +<# + Starting with PowerShell v3, modules are auto-imported when needed. Importing the module here + ensures clarity and avoids ambiguity. +#> + +# Import-Module AsBuiltReport.Chart -Force -Verbose:$false + +<# + Since the chart output is a file, specify the output folder path using $OutputFolderPath. +#> + +$OutputFolderPath = Resolve-Path $Path + +<# + Define the data to be displayed in the chart. + In a real-world scenario these values would come from your infrastructure query. +#> + +$ChartTitle = 'Security Posture Assessment' +$Values = @(@(1, 2, 5, 8), @(3, 5, 4, 2)) +$Labels = @('USA DataCenter', 'UK DataCenter') +$Spokes = @('Network Security', 'Endpoint Security', 'Identity Management', 'Data Protection') + +<# + The New-RadarChart cmdlet generates the Radar Chart image. + + -Title : Sets the chart title displayed at the top of the image. + -Values : Array of numeric values, one per axis. + -LegendLabels : Array of label strings corresponding to each value. + -SpokeLabels : Array of label strings for each spoke on the radar chart. + -SpokesLength : Length of the spokes (axes) in the radar chart. + -Format : Output file format (e.g. png, jpg, svg). + -OutputFolderPath : Directory where the generated chart file will be saved. + -Width : Width of the chart image in pixels. + -Height : Height of the chart image in pixels. + -ColorPalette : Predefined color palette (e.g. Category20, Pastel). + -Filename : Name of the output file (without extension). +#> + +New-RadarChart ` + -Title $ChartTitle ` + -Values $Values ` + -LegendLabels $Labels ` + -SpokeLabels $Spokes ` + -SpokesLength 9 ` + -Format $Format ` + -EnableLegend ` + -OutputFolderPath $OutputFolderPath ` + -Width 600 ` + -Height 400 ` + -ColorPalette Aurora ` + -Filename 'Example15-RadarChart' \ No newline at end of file diff --git a/Sources/DonutChart.cs b/Sources/DonutChart.cs index a6464ac..ae880ff 100644 --- a/Sources/DonutChart.cs +++ b/Sources/DonutChart.cs @@ -112,6 +112,16 @@ public object Chart(double[] values, string[] labels, string filename = "output" // Set label distance from the center of the donut slices pie.SliceLabelDistance = _labelDistance; + if (DonutCenterText != null) + { + var annotation = myPlot.Add.Annotation(DonutCenterText); + annotation.Alignment = Alignment.MiddleCenter; + annotation.LabelStyle.FontSize = 12; + annotation.LabelStyle.BackgroundColor = Colors.White; + annotation.LabelStyle.BorderColor = Colors.White; + annotation.LabelShadowColor = Colors.Transparent; + } + // Apply watermark if enabled ApplyWatermark(myPlot); @@ -127,6 +137,4 @@ public object Chart(double[] values, string[] labels, string filename = "output" } } } -} - - +} \ No newline at end of file diff --git a/Sources/PowerShell/RadarChartPwsh.cs b/Sources/PowerShell/RadarChartPwsh.cs index a9a362e..ca03bdd 100644 --- a/Sources/PowerShell/RadarChartPwsh.cs +++ b/Sources/PowerShell/RadarChartPwsh.cs @@ -97,6 +97,11 @@ public class NewRadarChartCommand : Cmdlet [Parameter(Mandatory = false, HelpMessage = "Switch to make label font bold.")] public SwitchParameter LabelBold { get; set; } + // this set the distance of the labels from the chart center (Radar Chart) + [Parameter(Mandatory = false, HelpMessage = "Distance of labels from the chart center (0.5 to 0.9). Defaults to 0.6.")] + [ValidateSet("0.5", "0.6", "0.7", "0.8", "0.9")] + public double LabelDistance { get; set; } = 0.8; + // this set the distance of the labels from the chart center (Radar Chart) [Parameter(Mandatory = false, HelpMessage = "Distance of labels from the chart center (10 to 50). Defaults to 10.")] public double SpokesLength { get; set; } = 10; @@ -221,9 +226,12 @@ protected override void ProcessRecord() Chart.TitleFontColor = TitleFontColor; } - // This set the distance of the labels from the chart center (Radar Chart) + // This set the distance of the spokes from the chart center (Radar Chart) Chart.SpokesLength = SpokesLength; + // This set the distance of the labels from the chart center (Radar Chart) + Chart.LabelDistance = LabelDistance; + // Font Settings Chart.FontName = FontName; Chart.LabelFontSize = LabelFontSize; diff --git a/Sources/RadarChart.cs b/Sources/RadarChart.cs index 86c6af1..8a41326 100644 --- a/Sources/RadarChart.cs +++ b/Sources/RadarChart.cs @@ -165,6 +165,7 @@ public object Chart(List values, string[] labels, string[] categoryNam radar.PolarAxis.Spokes[i].LabelStyle.ForeColor = GetDrawingColor(LabelFontColor); radar.PolarAxis.Spokes[i].LabelStyle.Bold = LabelBold; radar.PolarAxis.Spokes[i].LabelStyle.FontName = FontName; + radar.PolarAxis.Spokes[i].LabelPaddingFraction = LabelDistance; } } else From d3799ffc17eb1470e59d8c927ac4db08f52d4a12 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 11 Jun 2026 22:16:43 -0400 Subject: [PATCH 12/16] Fix array initialization for chart values in Example14.ps1 --- Examples/Example14.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example14.ps1 b/Examples/Example14.ps1 index 37845ab..ebf3b19 100644 --- a/Examples/Example14.ps1 +++ b/Examples/Example14.ps1 @@ -32,7 +32,7 @@ $OutputFolderPath = Resolve-Path $Path #> $ChartTitle = 'Security Posture Assessment' -$Values = @(@(3,5,4,2)) +$Values = ,@(3,5,4,2) # The comma before the array ensures it's treated as a single array object $Labels = @('USA DataCenter') <# From 8c318e0b4a0b3170adbff69c7b307f2e9a6a4c50 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Fri, 12 Jun 2026 09:44:56 -0400 Subject: [PATCH 13/16] Update Example05.ps1 --- Examples/Example05.ps1 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Examples/Example05.ps1 b/Examples/Example05.ps1 index 180fff1..38faaf0 100644 --- a/Examples/Example05.ps1 +++ b/Examples/Example05.ps1 @@ -45,12 +45,11 @@ $OutputFolderPath = Resolve-Path $Path #> $ChartTitle = 'Datastore Capacity (GB)' -$Labels = @('datastore-01', 'datastore-02', 'datastore-03', 'datastore-04') +$Labels = @('datastore-01') $LegendCategories = @('Used Space', 'Free Space') # Each inner array represents one bar's segment values, ordered by $LegendCategories. -$Values = @(@(800, 200), @(600, 400), @(1500, 500), @(300, 700)) - +$Values = ,@(800, 200) # The comma before the array creates a single-element array containing the inner array, which is required for the Stacked Bar Chart format. <# The New-StackedBarChart cmdlet generates the Stacked Bar Chart image. From 39527814d6b98ecff1ee71a84cd9a4a242dd6117 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Fri, 12 Jun 2026 10:16:23 -0400 Subject: [PATCH 14/16] Update copilot instructions and add SpokesLength property to Chart class --- .github/copilot-instructions.md | 28 ++++++++++++++++++++++++---- Sources/Chart.cs | 1 + 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2d86f45..e414f67 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -5,16 +5,18 @@ This project is a **hybrid C# + PowerShell module**. The C# layer compiles into platform-specific DLLs; the PowerShell layer loads the right DLL at import time and exposes cmdlets. ``` -Sources/ ← C# library (ScottPlot + SkiaSharp, netstandard2.0) +Sources/ ← C# library (ScottPlot + SkiaSharp, net8.0 / netstandard2.0) Chart.cs ← Static base class; all chart settings live here as static props - PieChart.cs / BarChart.cs / StackedBarChart.cs / SignalChart.cs ← Chart implementations + PieChart.cs / DonutChart.cs / BarChart.cs / StackedBarChart.cs / SignalChart.cs / RadarChart.cs + ← Chart implementations PowerShell/ ← One PSCmdlet class per chart type (New-*Chart cmdlets) Enums/Enums.cs ← All shared enums (BasicColors, Formats, ColorPalettes, etc.) AsBuiltReport.Chart/ ← PowerShell module AsBuiltReport.Chart.psm1 ← Loads the correct DLL based on PSEdition + OS + architecture - AsBuiltReport.Chart.psd1 ← Module manifest; exports: New-PieChart, New-BarChart, - New-StackedBarChart, New-SingleStackedBarChart, New-SignalChart + AsBuiltReport.Chart.psd1 ← Module manifest; exports: New-PieChart, New-DonutChart, + New-BarChart, New-StackedBarChart, New-SingleStackedBarChart, + New-SignalChart, New-RadarChart Src/Assemblies/ ← Pre-compiled DLLs, organized by platform: Core/linux-x64/ Core/windows-x64/ @@ -70,8 +72,26 @@ Invoke-ScriptAnalyzer -Path . -Recurse -Settings .\.github\workflows\PSScriptAna Excluded rules: `PSUseToExportFieldsInManifest`, `PSReviewUnusedParameter`, `PSUseDeclaredVarsMoreThanAssignments`, `PSAvoidGlobalVars`. +### Build (Pester Tests) in CI/CD + +```powershell +.\Tests\Invoke-Tests.ps1 -CodeCoverage -OutputFormat NUnitXml +``` + +Supports optional flags: +- `-CodeCoverage` – Enable code coverage analysis +- `-OutputFormat ` – Output format for test results (default: Console) + ## Key Conventions +### Cross-platform DLL loading + +The `.psm1` module import process detects the current PowerShell edition, OS, and architecture, then loads the appropriate pre-compiled DLL: +- **PowerShell 7+** on Windows/Linux/macOS → Loads from `Core/` folder +- **PowerShell 5.1 (Desktop)** on Windows → Loads from `Desktop/windows-x64/` folder (Windows-only) + +If the correct DLL cannot be found for the runtime, the module import fails with a clear error message. + ### Adding a new chart type Every new chart type requires changes in both layers: diff --git a/Sources/Chart.cs b/Sources/Chart.cs index fc574c4..318caa5 100644 --- a/Sources/Chart.cs +++ b/Sources/Chart.cs @@ -483,6 +483,7 @@ internal static void Reset() _watermarkOpacity = 0.3; DonutFraction = 0.5; HideValues = false; + SpokesLength = 10; } public static string GenerateToken(Byte length) From 5c56c3b454caa85f20ba1e23d2dcdda40431997f Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Fri, 12 Jun 2026 11:15:32 -0400 Subject: [PATCH 15/16] Add tests for New-RadarChart functionality and export verification --- Tests/AsBuiltReport.Chart.Functions.Tests.ps1 | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 index faba0c6..3264829 100644 --- a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 +++ b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 @@ -23,6 +23,12 @@ Describe 'AsBuiltReport.Chart Exported Functions' { It 'Should export New-SingleStackedBarChart' { Get-Command -Module AsBuiltReport.Chart -Name New-SingleStackedBarChart | Should -Not -BeNullOrEmpty } + It 'Should export New-DonutChart' { + Get-Command -Module AsBuiltReport.Chart -Name New-DonutChart | Should -Not -BeNullOrEmpty + } + It 'Should export New-RadarChart' { + Get-Command -Module AsBuiltReport.Chart -Name New-RadarChart | Should -Not -BeNullOrEmpty + } Context 'New-PieChart' { It 'Should run without error with sample input' { @@ -234,6 +240,62 @@ Describe 'AsBuiltReport.Chart Exported Functions' { } } + Context 'New-RadarChart' { + It 'Should run without error with sample input' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + It 'Should return a file path as output' { + $result = New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive + $result | Should -BeOfType 'System.IO.FileSystemInfo' + Test-Path $result | Should -BeTrue + } + It 'Should throw error for missing mandatory parameters' { + { New-RadarChart } | Should -Throw + } + It 'Should throw error for missing Title' { + { New-RadarChart -Values @(@(1, 2, 5, 8)) -LegendLabels @('A') -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw + } + It 'Should throw error for missing Values' { + { New-RadarChart -Title 'Test' -LegendLabels @('A') -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw + } + It 'Should throw error for missing LegendLabels' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8)) -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw + } + It 'Should throw error for mismatched Values and LegendLabels' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A') -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw -ExpectedMessage "Error: Values and labels must be equal." + } + It 'Should throw error for mismatched Values elements and SpokeLabels' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw -ExpectedMessage "Every collection in the series must have the same number of items" + } + It 'Should run without error with legend enabled' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -EnableLegend } | Should -Not -Throw + } + It 'Should run without error with legend positioned horizontally' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -EnableLegend -LegendOrientation Horizontal -LegendAlignment UpperCenter } | Should -Not -Throw + } + It 'Should run without error with chart border enabled' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -EnableChartBorder } | Should -Not -Throw + } + It 'Should run without error with custom color palette' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -EnableCustomColorPalette -CustomColorPalette @('#4472C4', '#ED7D31') } | Should -Not -Throw + } + It 'Should run without error with bold title' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -TitleFontBold } | Should -Not -Throw + } + It 'Should run without error with custom title font size' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -TitleFontSize 20 } | Should -Not -Throw + } + It 'Should run without error with custom label font size' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -LabelFontSize 12 } | Should -Not -Throw + } + It 'Should run without error with custom filename' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Filename 'MyRadarChart' -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + It 'Should run without error without SpokeLabels' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + } + Context 'Watermark support' { Context 'New-PieChart with watermark' { It 'Should run without error with watermark enabled using defaults' { @@ -308,6 +370,23 @@ Describe 'AsBuiltReport.Chart Exported Functions' { { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3, 4)) -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } } + + Context 'New-RadarChart with watermark' { + It 'Should run without error with watermark enabled using defaults' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark } | Should -Not -Throw + } + It 'Should return a file when watermark is enabled' { + $result = New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark + $result | Should -BeOfType 'System.IO.FileSystemInfo' + Test-Path $result | Should -BeTrue + } + It 'Should run without error with custom watermark text, color, and opacity' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark -WatermarkText 'DRAFT' -WatermarkColor Blue -WatermarkOpacity 0.25 } | Should -Not -Throw + } + It 'Should run without error without watermark (disabled by default)' { + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + } } Context 'Output format file extensions' { @@ -331,6 +410,16 @@ Describe 'AsBuiltReport.Chart Exported Functions' { $result.Extension | Should -Be '.jpeg' Test-Path $result | Should -BeTrue } + It 'New-RadarChart with Format jpg should produce a .jpg file' { + $result = New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'jpg' -OutputFolderPath $TestDrive + $result.Extension | Should -Be '.jpg' + Test-Path $result | Should -BeTrue + } + It 'New-RadarChart with Format jpeg should produce a .jpeg file' { + $result = New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'jpeg' -OutputFolderPath $TestDrive + $result.Extension | Should -Be '.jpeg' + Test-Path $result | Should -BeTrue + } } Context 'State isolation between cmdlet calls' { @@ -352,5 +441,9 @@ Describe 'AsBuiltReport.Chart Exported Functions' { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3)) -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark | Out-Null { New-SignalChart -Title 'Test' -Values @(, [double[]]@(1, 2, 3)) -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw } + It 'EnableLegend should not persist from one New-RadarChart call to the next' { + New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive -EnableLegend | Out-Null + { New-RadarChart -Title 'Test' -Values @(@(1, 2, 5, 8), @(3, 5, 4, 2)) -LegendLabels @('A', 'B') -SpokeLabels @('Wins', 'Poles', 'Podiums', 'Points') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } } } From b783bbb177c6bb4bd0d165178042fd0336b5e6b3 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Fri, 12 Jun 2026 12:16:20 -0400 Subject: [PATCH 16/16] Update changelog for version 0.3.3 release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc64ce7..9caa7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.3.3] - Unreleased +## [0.3.3] - 2026-06-12 ### Added