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/.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..9caa7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ 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] - 2026-06-12 + +### 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 + - 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 + +- 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/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. diff --git a/Examples/Example12.ps1 b/Examples/Example12.ps1 new file mode 100644 index 0000000..8da6115 --- /dev/null +++ b/Examples/Example12.ps1 @@ -0,0 +1,61 @@ +<# + .SYNOPSIS + Example 12 - 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..3bf3186 --- /dev/null +++ b/Examples/Example13.ps1 @@ -0,0 +1,98 @@ +<# + .SYNOPSIS + 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: + - 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/Examples/Example14.ps1 b/Examples/Example14.ps1 new file mode 100644 index 0000000..ebf3b19 --- /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) # The comma before the array ensures it's treated as a single array object +$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/README.md b/README.md index 89ab0de..bbb6f6b 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,21 +111,41 @@ 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. -| 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 | +| [Example14](./Examples/Example14.ps1) | Basic Radar Chart with Legend | +| [Example15](./Examples/Example15.ps1) | Radar Chart with Legend, Custom Colors and Border | ## Watermark Support @@ -126,14 +153,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 @@ -157,9 +184,3 @@ New-SignalChart -Title 'Throughput' -Values @(,[double[]]@(1,2,3,4,5)) -Format ' ## :x: Known Issues - No known issues at this time. - - - - - - diff --git a/Samples/DonutChart.png b/Samples/DonutChart.png new file mode 100644 index 0000000..c663c0b Binary files /dev/null and b/Samples/DonutChart.png differ diff --git a/Samples/RadarChart.png b/Samples/RadarChart.png new file mode 100644 index 0000000..dfaed97 Binary files /dev/null and b/Samples/RadarChart.png differ diff --git a/Samples/SingleStackedBarChart.png b/Samples/SingleStackedBarChart.png new file mode 100644 index 0000000..0524892 Binary files /dev/null and b/Samples/SingleStackedBarChart.png differ 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..318caa5 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,9 @@ internal static void Reset() WatermarkFontSize = 24; WatermarkColor = BasicColors.Gray; _watermarkOpacity = 0.3; + DonutFraction = 0.5; + HideValues = false; + SpokesLength = 10; } public static string GenerateToken(Byte length) diff --git a/Sources/DonutChart.cs b/Sources/DonutChart.cs new file mode 100644 index 0000000..ae880ff --- /dev/null +++ b/Sources/DonutChart.cs @@ -0,0 +1,140 @@ +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; + + 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); + + // 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."); + } + } + } +} \ No newline at end of file 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..ca03bdd --- /dev/null +++ b/Sources/PowerShell/RadarChartPwsh.cs @@ -0,0 +1,277 @@ +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 (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; + + // 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 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; + 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..8a41326 --- /dev/null +++ b/Sources/RadarChart.cs @@ -0,0 +1,252 @@ +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; + radar.PolarAxis.Spokes[i].LabelPaddingFraction = LabelDistance; + } + } + 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..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' { @@ -41,6 +47,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 +100,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 +149,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 +161,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 +177,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 +185,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 @@ -217,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' { @@ -277,18 +356,35 @@ 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 + } + } + + 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 } } } @@ -314,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' { @@ -332,8 +438,12 @@ 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 + } + 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 } } } 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