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..4fa00be 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 = @() @@ -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', '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 82ca8ca..ddc3341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ 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 + +### 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 radar chart support: implement New-RadarChart cmdlet and associated classes + +### 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.2] - 2026-05-05 ### Added 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/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 - + - - + + diff --git a/Sources/Chart.cs b/Sources/Chart.cs index 4c5834d..fc574c4 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; } @@ -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,27 @@ 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 + { + 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 +126,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 +481,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/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/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() 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/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 } } } diff --git a/Todo.md b/Todo.md index 8b13789..d6cc011 100644 --- a/Todo.md +++ b/Todo.md @@ -1 +1,2 @@ - +[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