From 2548e7519c2edd6ada6c9bd5bb830ffa20eeb088 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 21 Feb 2026 10:51:56 -0400 Subject: [PATCH 01/62] Fix Bug report (#5) * Enhance module manifest with additional metadata including tags, license, project URL, icon, and release notes * Add validation for category names in StackedBarChart and implement tests for chart functions * Update Pester workflow to use actions/checkout@v6 and setup .NET 8.0, streamline build process for MacOS, Linux, and Windows * Enhance Pester workflow to include conditional checks for OS-specific build and copy steps * Add Windows PowerShell build and copy steps to Pester workflow * Enhance Pester and Release workflows for MacOS ARM64 support and update architecture handling in PowerShell module * Remove unnecessary module dependencies from Invoke-Tests script and clean up Pester workflow * Remove conditional code coverage for Windows PowerShell in Pester workflow * Enhance module import statements for macOS ARM and x86 architectures with verbose and debug output * Refactor workflows and issue templates for consistency and clarity; enhance Pester test output with verbose and debug options * Update Pester workflow to list DLL files from macOS ARM directory * Fix path for listing DLL files in macOS ARM directory in Pester workflow * Fix path for listing DLL files in macOS ARM directory in Pester workflow * Update paths for copying libraries and listing DLL files in Pester workflow * Update Pester workflow to use PowerShell commands for copying libraries across platforms * Update Pester workflow to improve library copy commands and list DLL files for macOS ARM * Update Pester workflow to use Unix-style copy command for macOS library * Update Pester and Release workflows to use PowerShell copy commands and adjust library paths for macOS * Fix module import paths for macOS ARM and x64 architectures in AsBuiltReport.Chart.psm1 * Update module version to 0.2.0 and add changelog entry for new features * Update issue templates: enhance bug report and change request forms --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/change_request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1015b96..0b2e99d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,7 @@ name: Bug Report description: File a bug report labels: ["bug"] -assignees: +assignees: ["rebelinux"] body: - type: textarea id: bug-description diff --git a/.github/ISSUE_TEMPLATE/change_request.yml b/.github/ISSUE_TEMPLATE/change_request.yml index 8a2cee0..604e378 100644 --- a/.github/ISSUE_TEMPLATE/change_request.yml +++ b/.github/ISSUE_TEMPLATE/change_request.yml @@ -1,7 +1,7 @@ name: Change Request description: Request a new change or an improvement labels: ["change request"] -assignees: +assignees: ["rebelinux"] body: - type: textarea id: description From 1f2552a613b0f39828ac39ce1f078d3bcea0d899 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 21 Feb 2026 10:54:55 -0400 Subject: [PATCH 02/62] Clean up Release.yml by removing commented code Removed commented-out sections related to tweeting and Bluesky posting. --- .github/workflows/Release.yml | 53 ++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 46c2838..89652ea 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -49,29 +49,30 @@ 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 }} + # 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 }} + From f0b52684d661e5a376611fdbba7d8543cd55ba8b Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sun, 22 Feb 2026 20:48:19 -0400 Subject: [PATCH 03/62] Update README.md with new content and links --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fb45ac2..371a7b3 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The AsBuiltReport Chart As Built Report supports the following languages; ## :wrench: System Requirements -PowerShell 7, and the following PowerShell modules are required for generating a AsBuiltReport Chart. +PowerShell 5.1 or PowerShell 7, and the following PowerShell modules are required for generating a AsBuiltReport Chart. - [AsBuiltReport.Core Module](https://www.powershellgallery.com/packages/AsBuiltReport.Core/) @@ -107,3 +107,4 @@ The **Options** schema allows certain options within the report to be toggled on ## :computer: Examples + From 8ca909f0253a83767bb454b3403b9befbf82c191 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 23 Feb 2026 21:16:32 -0400 Subject: [PATCH 04/62] Revise README for AsBuiltReport.Chart module Updated README.md to reflect current module status and requirements. --- README.md | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 371a7b3..5d27652 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ PowerShell 5.1 or PowerShell 7, and the following PowerShell modules are require - [AsBuiltReport.Core Module](https://www.powershellgallery.com/packages/AsBuiltReport.Core/) ### :closed_lock_with_key: Required Privileges - - + +Local user privilege ## :package: Module Installation @@ -84,27 +84,7 @@ install-module AsBuiltReport.Chart -Force update-module AsBuiltReport.Chart -Force ``` -### GitHub - -If you are unable to use the PowerShell Gallery, you can still install the module manually. Ensure you repeat the following steps for the [system requirements](https://github.com/AsBuiltReport/AsBuiltReport.Chart#wrench-system-requirements) also. - -1. Download the code package / [latest release](https://github.com/AsBuiltReport/AsBuiltReport.Chart/releases/latest) zip from GitHub -2. Extract the zip file -3. Copy the folder `AsBuiltReport.Chart` to a path that is set in `$env:PSModulePath`. -4. Open a PowerShell terminal window and unblock the downloaded files with - - ```powershell - $path = (Get-Module -Name AsBuiltReport.Chart -ListAvailable).ModuleBase; Unblock-File -Path $path\*.psd1; Unblock-File -Path $path\Src\Public\*.ps1; Unblock-File -Path $path\Src\Private\*.ps1 - ``` - -5. Close and reopen the PowerShell terminal window. - -_Note: You are not limited to installing the module to those example paths, you can add a new entry to the environment variable PSModulePath if you want to use another path._ - -### Options - -The **Options** schema allows certain options within the report to be toggled on or off. - ## :computer: Examples + From 8f583ce129c3241cc1864aae2f12ba33d167c507 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 23 Feb 2026 21:45:11 -0400 Subject: [PATCH 05/62] Update Readme (#7) * Enhance module manifest with additional metadata including tags, license, project URL, icon, and release notes * Add validation for category names in StackedBarChart and implement tests for chart functions * Update Pester workflow to use actions/checkout@v6 and setup .NET 8.0, streamline build process for MacOS, Linux, and Windows * Enhance Pester workflow to include conditional checks for OS-specific build and copy steps * Add Windows PowerShell build and copy steps to Pester workflow * Enhance Pester and Release workflows for MacOS ARM64 support and update architecture handling in PowerShell module * Remove unnecessary module dependencies from Invoke-Tests script and clean up Pester workflow * Remove conditional code coverage for Windows PowerShell in Pester workflow * Enhance module import statements for macOS ARM and x86 architectures with verbose and debug output * Refactor workflows and issue templates for consistency and clarity; enhance Pester test output with verbose and debug options * Update Pester workflow to list DLL files from macOS ARM directory * Fix path for listing DLL files in macOS ARM directory in Pester workflow * Fix path for listing DLL files in macOS ARM directory in Pester workflow * Update paths for copying libraries and listing DLL files in Pester workflow * Update Pester workflow to use PowerShell commands for copying libraries across platforms * Update Pester workflow to improve library copy commands and list DLL files for macOS ARM * Update Pester workflow to use Unix-style copy command for macOS library * Update Pester and Release workflows to use PowerShell copy commands and adjust library paths for macOS * Fix module import paths for macOS ARM and x64 architectures in AsBuiltReport.Chart.psm1 * Update module version to 0.2.0 and add changelog entry for new features * Update issue templates: enhance bug report and change request forms * Fix validation check in Chart method to compare values count with category names length * Refactor module import statements for macOS architecture and update README with detailed module description and usage examples --- AsBuiltReport.Chart/AsBuiltReport.Chart.psm1 | 6 ++-- .../Src/Assemblies/Core/mac-osx/dummy | 0 README.md | 33 +++++++++++++++---- Sources/StackedBarChart.cs | 2 +- 4 files changed, 31 insertions(+), 10 deletions(-) delete mode 100644 AsBuiltReport.Chart/Src/Assemblies/Core/mac-osx/dummy diff --git a/AsBuiltReport.Chart/AsBuiltReport.Chart.psm1 b/AsBuiltReport.Chart/AsBuiltReport.Chart.psm1 index 57d1e66..1aa45ab 100644 --- a/AsBuiltReport.Chart/AsBuiltReport.Chart.psm1 +++ b/AsBuiltReport.Chart/AsBuiltReport.Chart.psm1 @@ -5,15 +5,15 @@ switch ($PSVersionTable.PSEdition) { $architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture if ($architecture -eq "Arm64" -or $architecture -eq "Arm") { Write-Verbose "Architecture: ARM (Apple Silicon)" - Import-Module ("$PSScriptRoot{0}Src{0}Assemblies{0}Core{0}mac-osx{0}osx-arm64{0}AsBuiltReportChart.dll" -f [System.IO.Path]::DirectorySeparatorChar) -Verbose -Debug + Import-Module ("$PSScriptRoot{0}Src{0}Assemblies{0}Core{0}mac-osx{0}osx-arm64{0}AsBuiltReportChart.dll" -f [System.IO.Path]::DirectorySeparatorChar) } elseif ($architecture -eq "X64" -or $architecture -eq "X86") { Write-Verbose "Architecture: x86 (Intel)" - Import-Module ("$PSScriptRoot{0}Src{0}Assemblies{0}Core{0}mac-osx{0}osx-x64{0}AsBuiltReportChart.dll" -f [System.IO.Path]::DirectorySeparatorChar) -Verbose -Debug + Import-Module ("$PSScriptRoot{0}Src{0}Assemblies{0}Core{0}mac-osx{0}osx-x64{0}AsBuiltReportChart.dll" -f [System.IO.Path]::DirectorySeparatorChar) } else { Write-Verbose "Architecture: Unknown or other architecture" - Import-Module ("$PSScriptRoot{0}Src{0}Assemblies{0}Core{0}mac-osx{0}osx-arm64{0}AsBuiltReportChart.dll" -f [System.IO.Path]::DirectorySeparatorChar) -Verbose -Debug + Import-Module ("$PSScriptRoot{0}Src{0}Assemblies{0}Core{0}mac-osx{0}osx-arm64{0}AsBuiltReportChart.dll" -f [System.IO.Path]::DirectorySeparatorChar) } } elseif ($IsLinux) { Import-Module ("$PSScriptRoot{0}Src{0}Assemblies{0}Core{0}linux-x64{0}AsBuiltReportChart.dll" -f [System.IO.Path]::DirectorySeparatorChar) diff --git a/AsBuiltReport.Chart/Src/Assemblies/Core/mac-osx/dummy b/AsBuiltReport.Chart/Src/Assemblies/Core/mac-osx/dummy deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index 5d27652..c8b8be7 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,7 @@ ## :exclamation: THIS ASBUILTREPORT MODULE IS CURRENTLY IN DEVELOPMENT AND MIGHT NOT YET BE FUNCTIONAL ❗ -AsBuiltReport.Chart is a PowerShell module which works in conjunction with [AsBuiltReport.Core](https://github.com/AsBuiltReport/AsBuiltReport.Core). - -[AsBuiltReport](https://github.com/AsBuiltReport/AsBuiltReport) is an open-sourced community project which utilises PowerShell to produce as-built documentation in multiple document formats for multiple vendors and technologies. - -Please refer to the AsBuiltReport [website](https://www.asbuiltreport.com) for more detailed information about this project. +AsBuiltReport.Chart is a PowerShell module which provides a set of cmdlets for generating charts and visualizations in As Built Reports. This module is designed to work seamlessly with the AsBuiltReport.Core module, allowing users to create visually appealing and informative reports with ease. # :beginner: Getting Started @@ -85,6 +81,31 @@ update-module AsBuiltReport.Chart -Force ``` ## :computer: Examples - +Here are some examples to get you going. + +### Pie Chart Examples +```powershell +# Generate a Pie 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-PieChart -Title 'Test' -Values @(1,2) -Labels @('A','B') -Format 'png' -EnableLegend -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 +``` +![PieChart](./Samples/PieChart.png) + +### Bar Chart Examples +```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. +New-BarChart -Title 'Test' -Values @(1,2) -Labels @('A','B') -Format 'png' -EnableLegend -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 -AxesMarginsTop 1 +``` +![BarChart](./Samples/BarChart.png) + +### Stacked Bar Chart Examples +```powershell +# Generate a Stacked Bar Chart with the title 'Test', values of 1 and 2 for the first category and 3 and 4 for the second category, labels 'A' and 'B', legend categories 'Value1' and 'Value2', 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-StackedBarChart -Title 'Test' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -LegendCategories @('Value1','Value2') -Format 'png' -EnableLegend -LegendOrientation Horizontal -LegendAlignment UpperCenter -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 -AxesMarginsTop 1 +``` +![StackedBarChart](./Samples/StackedBarChart.png) + +## :x: Known Issues + + - No known issues at this time. diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 20e945e..8ed2a5f 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -10,7 +10,7 @@ internal class StackedBar : Chart static StackedBar() { } public object Chart(List values, string[] labels, string[] categoryNames, string filename = "output", int width = 400, int height = 300) { - if (values.Count != categoryNames.Length) + if (values[0].Length != categoryNames.Length) { throw new Exception("Error: Values and category names must be equal."); } From a7382087bd15a65a2f7ab75c7093fb63c4f56644 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 23 Feb 2026 21:49:56 -0400 Subject: [PATCH 06/62] Add sample chart images for BarChart, PieChart, and StackedBarChart (#8) * Enhance module manifest with additional metadata including tags, license, project URL, icon, and release notes * Add validation for category names in StackedBarChart and implement tests for chart functions * Update Pester workflow to use actions/checkout@v6 and setup .NET 8.0, streamline build process for MacOS, Linux, and Windows * Enhance Pester workflow to include conditional checks for OS-specific build and copy steps * Add Windows PowerShell build and copy steps to Pester workflow * Enhance Pester and Release workflows for MacOS ARM64 support and update architecture handling in PowerShell module * Remove unnecessary module dependencies from Invoke-Tests script and clean up Pester workflow * Remove conditional code coverage for Windows PowerShell in Pester workflow * Enhance module import statements for macOS ARM and x86 architectures with verbose and debug output * Refactor workflows and issue templates for consistency and clarity; enhance Pester test output with verbose and debug options * Update Pester workflow to list DLL files from macOS ARM directory * Fix path for listing DLL files in macOS ARM directory in Pester workflow * Fix path for listing DLL files in macOS ARM directory in Pester workflow * Update paths for copying libraries and listing DLL files in Pester workflow * Update Pester workflow to use PowerShell commands for copying libraries across platforms * Update Pester workflow to improve library copy commands and list DLL files for macOS ARM * Update Pester workflow to use Unix-style copy command for macOS library * Update Pester and Release workflows to use PowerShell copy commands and adjust library paths for macOS * Fix module import paths for macOS ARM and x64 architectures in AsBuiltReport.Chart.psm1 * Update module version to 0.2.0 and add changelog entry for new features * Update issue templates: enhance bug report and change request forms * Fix validation check in Chart method to compare values count with category names length * Refactor module import statements for macOS architecture and update README with detailed module description and usage examples * Add sample chart images for BarChart, PieChart, and StackedBarChart --- Samples/BarChart.png | Bin 0 -> 14830 bytes Samples/PieChart.png | Bin 0 -> 13514 bytes Samples/StackedBarChart.png | Bin 0 -> 13048 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Samples/BarChart.png create mode 100644 Samples/PieChart.png create mode 100644 Samples/StackedBarChart.png diff --git a/Samples/BarChart.png b/Samples/BarChart.png new file mode 100644 index 0000000000000000000000000000000000000000..2080150d888eb1788ceac437053b89432388c256 GIT binary patch literal 14830 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�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 literal 0 HcmV?d00001 From 00f688862b573b9b112ab86b4bb2c7c90efea9f9 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 23 Feb 2026 21:51:55 -0400 Subject: [PATCH 07/62] Update README.md with new content and examples --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c8b8be7..207b6a1 100644 --- a/README.md +++ b/README.md @@ -83,21 +83,21 @@ update-module AsBuiltReport.Chart -Force ## :computer: Examples Here are some examples to get you going. -### Pie Chart Examples +### Pie Chart Example ```powershell # Generate a Pie 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-PieChart -Title 'Test' -Values @(1,2) -Labels @('A','B') -Format 'png' -EnableLegend -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 ``` ![PieChart](./Samples/PieChart.png) -### Bar Chart Examples +### Bar Chart Example ```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. New-BarChart -Title 'Test' -Values @(1,2) -Labels @('A','B') -Format 'png' -EnableLegend -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 -AxesMarginsTop 1 ``` ![BarChart](./Samples/BarChart.png) -### Stacked Bar Chart Examples +### Stacked Bar Chart Example ```powershell # Generate a Stacked Bar Chart with the title 'Test', values of 1 and 2 for the first category and 3 and 4 for the second category, labels 'A' and 'B', legend categories 'Value1' and 'Value2', 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-StackedBarChart -Title 'Test' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -LegendCategories @('Value1','Value2') -Format 'png' -EnableLegend -LegendOrientation Horizontal -LegendAlignment UpperCenter -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 -AxesMarginsTop 1 @@ -109,3 +109,4 @@ New-StackedBarChart -Title 'Test' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -L - No known issues at this time. + From 158e6bfbd4584848e2713e0d6bf94c9442a032eb Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 23 Feb 2026 21:53:43 -0400 Subject: [PATCH 08/62] Fix HTML encoding issues in README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 207b6a1..7dc9660 100644 --- a/README.md +++ b/README.md @@ -83,21 +83,21 @@ update-module AsBuiltReport.Chart -Force ## :computer: Examples Here are some examples to get you going. -### Pie Chart Example +### Pie Chart ```powershell # Generate a Pie 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-PieChart -Title 'Test' -Values @(1,2) -Labels @('A','B') -Format 'png' -EnableLegend -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 ``` ![PieChart](./Samples/PieChart.png) -### Bar Chart Example +### 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. New-BarChart -Title 'Test' -Values @(1,2) -Labels @('A','B') -Format 'png' -EnableLegend -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 -AxesMarginsTop 1 ``` ![BarChart](./Samples/BarChart.png) -### Stacked Bar Chart Example +### Stacked Bar Chart ```powershell # Generate a Stacked Bar Chart with the title 'Test', values of 1 and 2 for the first category and 3 and 4 for the second category, labels 'A' and 'B', legend categories 'Value1' and 'Value2', 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-StackedBarChart -Title 'Test' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -LegendCategories @('Value1','Value2') -Format 'png' -EnableLegend -LegendOrientation Horizontal -LegendAlignment UpperCenter -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 -AxesMarginsTop 1 @@ -110,3 +110,4 @@ New-StackedBarChart -Title 'Test' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -L + From dd28e9f835069e95eded1ab9433130847a5fb95e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:34:27 -0400 Subject: [PATCH 09/62] Fix stacked bar column filling entire chart area when single element is present (#10) * Initial plan * Fix: Set moderate bar width for single-element stacked bar charts Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> --- Sources/StackedBarChart.cs | 3 +++ Tests/AsBuiltReport.Chart.Functions.Tests.ps1 | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 8ed2a5f..b1d1c54 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -85,6 +85,8 @@ public object Chart(List values, string[] labels, string[] categoryNam // create bars var bars = new List(); + // Set a moderate bar width when there is only one element to prevent it from filling the entire chart area + double barSize = values.Count == 1 ? 0.5 : 0.8; // assign values and colors to each bar for (int x = 0; x < values.Count; x++) { @@ -101,6 +103,7 @@ public object Chart(List values, string[] labels, string[] categoryNam FillColor = colorPalette.GetColor(i), Label = $"{values[x][i]}", CenterLabel = true, + Size = barSize, }); nextBarBase += values[x][i]; } diff --git a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 index 6b29037..c946728 100644 --- a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 +++ b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 @@ -70,5 +70,13 @@ Describe 'AsBuiltReport.Chart Exported Functions' { It 'Should throw error for mismatched Values and LegendCategories' { { New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A', 'B') -LegendCategories @('X') -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw -ExpectedMessage "Error: Values and category names must be equal." } + 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 + } + 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 | Should -BeOfType 'System.IO.FileSystemInfo' + Test-Path $result | Should -BeTrue + } } } From afb95900e48a276542346f816d8592929855da5c Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 25 Feb 2026 16:05:04 -0400 Subject: [PATCH 10/62] Refactor StackedBarChart to improve error handling and streamline color palette logic --- Sources/StackedBarChart.cs | 334 +++++++++++++++++++------------------ 1 file changed, 173 insertions(+), 161 deletions(-) diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index b1d1c54..0d7f873 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -10,84 +10,78 @@ internal class StackedBar : Chart static StackedBar() { } public object Chart(List values, string[] labels, string[] categoryNames, string filename = "output", int width = 400, int height = 300) { - if (values[0].Length != categoryNames.Length) - { - throw new Exception("Error: Values and category names must be equal."); - } - if (values.Count == labels.Length) - { - Plot myPlot = new Plot(); + Plot myPlot = new Plot(); - if (EnableCustomColorPalette) + if (EnableCustomColorPalette) + { + if (_customColorPalette != null && _customColorPalette.Length > 0) { - if (_customColorPalette != null && _customColorPalette.Length > 0) - { - myPlot.Add.Palette = colorPalette = new ScottPlot.Palettes.Custom(_customColorPalette); - } - else - { - throw new Exception("CustomColorPalette is empty. Please provide valid color values."); - } + myPlot.Add.Palette = colorPalette = new ScottPlot.Palettes.Custom(_customColorPalette); } else { - // Set ScottPlot native color palette - if (colorPalette != null) - { - myPlot.Add.Palette = colorPalette; - } - } - - // Set X and Y axis label settings - if (AreaOrientation == Orientations.Horizontal) - { - myPlot.Axes.Bottom.Label.Text = LabelYAxis; - } - else if (AreaOrientation == Orientations.Vertical) - { - myPlot.Axes.Bottom.Label.Text = LabelXAxis; + throw new Exception("CustomColorPalette is empty. Please provide valid color values."); } - else + } + else + { + // Set ScottPlot native color palette + if (colorPalette != null) { - myPlot.Axes.Bottom.Label.Text = ""; + myPlot.Add.Palette = colorPalette; } - myPlot.Axes.Bottom.Label.FontSize = LabelFontSize; - myPlot.Axes.Bottom.Label.ForeColor = GetDrawingColor(LabelFontColor); - myPlot.Axes.Bottom.Label.FontName = FontName; + } - myPlot.Axes.Bottom.TickLabelStyle.FontSize = LabelFontSize; - myPlot.Axes.Bottom.TickLabelStyle.ForeColor = GetDrawingColor(LabelFontColor); - myPlot.Axes.Bottom.TickLabelStyle.FontName = FontName; - myPlot.Axes.Bottom.Label.Bold = LabelBold; + // Set X and Y axis label settings + if (AreaOrientation == Orientations.Horizontal) + { + myPlot.Axes.Bottom.Label.Text = LabelYAxis; + } + else if (AreaOrientation == Orientations.Vertical) + { + myPlot.Axes.Bottom.Label.Text = LabelXAxis; + } + else + { + myPlot.Axes.Bottom.Label.Text = ""; + } + myPlot.Axes.Bottom.Label.FontSize = LabelFontSize; + myPlot.Axes.Bottom.Label.ForeColor = GetDrawingColor(LabelFontColor); + myPlot.Axes.Bottom.Label.FontName = FontName; - // myPlot.Axes.Bottom.TickLabelStyle.Rotation = -10; + myPlot.Axes.Bottom.TickLabelStyle.FontSize = LabelFontSize; + myPlot.Axes.Bottom.TickLabelStyle.ForeColor = GetDrawingColor(LabelFontColor); + myPlot.Axes.Bottom.TickLabelStyle.FontName = FontName; + myPlot.Axes.Bottom.Label.Bold = LabelBold; - if (AreaOrientation == Orientations.Horizontal) - { - myPlot.Axes.Left.Label.Text = LabelXAxis; - } - else if (AreaOrientation == Orientations.Vertical) - { - myPlot.Axes.Left.Label.Text = LabelYAxis; - } - else - { - myPlot.Axes.Left.Label.Text = ""; - } - myPlot.Axes.Left.Label.FontSize = LabelFontSize; - myPlot.Axes.Left.Label.ForeColor = GetDrawingColor(LabelFontColor); - myPlot.Axes.Left.Label.FontName = FontName; - myPlot.Axes.Left.Label.Bold = LabelBold; + // myPlot.Axes.Bottom.TickLabelStyle.Rotation = -10; - myPlot.Axes.Left.TickLabelStyle.FontSize = LabelFontSize; - myPlot.Axes.Left.TickLabelStyle.ForeColor = GetDrawingColor(LabelFontColor); - myPlot.Axes.Left.TickLabelStyle.FontName = FontName; + if (AreaOrientation == Orientations.Horizontal) + { + myPlot.Axes.Left.Label.Text = LabelXAxis; + } + else if (AreaOrientation == Orientations.Vertical) + { + myPlot.Axes.Left.Label.Text = LabelYAxis; + } + else + { + myPlot.Axes.Left.Label.Text = ""; + } + myPlot.Axes.Left.Label.FontSize = LabelFontSize; + myPlot.Axes.Left.Label.ForeColor = GetDrawingColor(LabelFontColor); + myPlot.Axes.Left.Label.FontName = FontName; + myPlot.Axes.Left.Label.Bold = LabelBold; - // create bars - var bars = new List(); - // Set a moderate bar width when there is only one element to prevent it from filling the entire chart area - double barSize = values.Count == 1 ? 0.5 : 0.8; - // assign values and colors to each bar + myPlot.Axes.Left.TickLabelStyle.FontSize = LabelFontSize; + myPlot.Axes.Left.TickLabelStyle.ForeColor = GetDrawingColor(LabelFontColor); + myPlot.Axes.Left.TickLabelStyle.FontName = FontName; + + // create bars + var bars = new List(); + // assign values and colors to each bar + if (values.Count > 0 && values[0].Length > 1) + { for (int x = 0; x < values.Count; x++) { double nextBarBase = 0; @@ -103,140 +97,158 @@ public object Chart(List values, string[] labels, string[] categoryNam FillColor = colorPalette.GetColor(i), Label = $"{values[x][i]}", CenterLabel = true, - Size = barSize, }); nextBarBase += values[x][i]; } } } + } + else + { + // Set a moderate bar width when there is only one element to prevent it from filling the entire chart area + // double barSize = values.Count == 1 ? 5.0 : 8.0; + double nextBarBase = 0; + for (int x = 0; x < values.Count; x++) + { + if (colorPalette != null) + { + bars.Add(new ScottPlot.Bar + { + Position = 0, + Value = nextBarBase + values[x][0], + ValueBase = nextBarBase, + FillColor = colorPalette.GetColor(x), + Label = $"{values[x][0]}", + CenterLabel = true, + // Size = barSize, + }); + nextBarBase += values[x][0]; + } + } + } - // add bars to plot - var bar = myPlot.Add.Bars(bars); + // add bars to plot + var bar = myPlot.Add.Bars(bars); - // Customize bars label style, including color - bar.ValueLabelStyle.FontName = FontName; - bar.ValueLabelStyle.ForeColor = GetDrawingColor(LabelFontColor); - bar.ValueLabelStyle.Bold = LabelBold; - bar.ValueLabelStyle.FontSize = LabelFontSize; + // Customize bars label style, including color + bar.ValueLabelStyle.FontName = FontName; + bar.ValueLabelStyle.ForeColor = GetDrawingColor(LabelFontColor); + bar.ValueLabelStyle.Bold = LabelBold; + bar.ValueLabelStyle.FontSize = LabelFontSize; - // set each slice value to its label - ScottPlot.TickGenerators.NumericManual tickGen = new ScottPlot.TickGenerators.NumericManual(); + // set each slice value to its label + ScottPlot.TickGenerators.NumericManual tickGen = new ScottPlot.TickGenerators.NumericManual(); - // assign labels to each bar - if (AreaOrientation == Orientations.Vertical) + // assign labels to each bar + if (AreaOrientation == Orientations.Vertical) + { + for (var i = 0; i < categoryNames.Length; i++) { - for (var i = 0; i < categoryNames.Length; i++) + if (colorPalette != null && EnableLegend) { - if (colorPalette != null && EnableLegend) + myPlot.Legend.ManualItems.Add(new LegendItem() { - myPlot.Legend.ManualItems.Add(new LegendItem() - { - LabelText = categoryNames[i], - FillColor = colorPalette.GetColor(i) - }); - } + LabelText = categoryNames[i], + FillColor = colorPalette.GetColor(i) + }); } + } - for (var i = 0; i < labels.Length; i++) - { - // set ticks - tickGen.AddMajor(i, labels[i]); - } - myPlot.Axes.Bottom.TickGenerator = tickGen; + for (var i = 0; i < labels.Length; i++) + { + // set ticks + tickGen.AddMajor(i, labels[i]); } - else + myPlot.Axes.Bottom.TickGenerator = tickGen; + } + else + { + for (var i = 0; i < categoryNames.Length; i++) { - for (var i = 0; i < categoryNames.Length; i++) + if (colorPalette != null && EnableLegend) { - if (colorPalette != null && EnableLegend) + myPlot.Legend.ManualItems.Add(new LegendItem() { - myPlot.Legend.ManualItems.Add(new LegendItem() - { - LabelText = categoryNames[i], - FillColor = colorPalette.GetColor(i) - }); - } - } - - for (var i = 0; i < labels.Length; i++) - { - // set ticks - tickGen.AddMajor(i, labels[i]); + LabelText = categoryNames[i], + FillColor = colorPalette.GetColor(i) + }); } - // set ticks for horizontal orientation - myPlot.Axes.Left.TickGenerator = tickGen; } - bar.Horizontal = (AreaOrientation == Orientations.Horizontal); - + for (var i = 0; i < labels.Length; i++) + { + // set ticks + tickGen.AddMajor(i, labels[i]); + } + // set ticks for horizontal orientation + myPlot.Axes.Left.TickGenerator = tickGen; + } - // hide unnecessary plot components - myPlot.HideGrid(); - myPlot.Axes.Top.IsVisible = false; - myPlot.Axes.Right.IsVisible = false; + bar.Horizontal = (AreaOrientation == Orientations.Horizontal); - if (EnableLegend) - { - myPlot.ShowLegend(); - // Legend Font Properties - myPlot.Legend.FontName = FontName; - myPlot.Legend.FontSize = LegendFontSize; - myPlot.Legend.FontColor = GetDrawingColor(LegendFontColor); + // hide unnecessary plot components + myPlot.HideGrid(); + myPlot.Axes.Top.IsVisible = false; + myPlot.Axes.Right.IsVisible = false; - // Legend box Style Properties - myPlot.Legend.OutlineColor = GetDrawingColor(LegendBorderColor); - myPlot.Legend.OutlineWidth = LegendBorderSize; + if (EnableLegend) + { + myPlot.ShowLegend(); - myPlot.Legend.OutlinePattern = LegendBorderStyleMap[LegendBorderStyle]; + // Legend Font Properties + myPlot.Legend.FontName = FontName; + myPlot.Legend.FontSize = LegendFontSize; + myPlot.Legend.FontColor = GetDrawingColor(LegendFontColor); - myPlot.Legend.Orientation = LegendOrientationMap[LegendOrientation]; + // Legend box Style Properties + myPlot.Legend.OutlineColor = GetDrawingColor(LegendBorderColor); + myPlot.Legend.OutlineWidth = LegendBorderSize; - myPlot.Legend.Alignment = LegendAlignmentMap[LegendAlignment]; - } + myPlot.Legend.OutlinePattern = LegendBorderStyleMap[LegendBorderStyle]; - if (EnableChartBorder) - { - myPlot.FigureBorder = new LineStyle() - { - // Set chart border properties - Color = GetDrawingColor(ChartBorderColor), - Width = ChartBorderSize, - Pattern = ChartBorderStyleMap[ChartBorderStyle], - }; - } + myPlot.Legend.Orientation = LegendOrientationMap[LegendOrientation]; - // 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; - } + myPlot.Legend.Alignment = LegendAlignmentMap[LegendAlignment]; + } - // Set margins settings - if (AreaOrientation == Orientations.Horizontal) - { - myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); - } - else + if (EnableChartBorder) + { + myPlot.FigureBorder = new LineStyle() { - myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); - } - - // Set filepath + // Set chart border properties + Color = GetDrawingColor(ChartBorderColor), + Width = ChartBorderSize, + Pattern = ChartBorderStyleMap[ChartBorderStyle], + }; + } - string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); + // 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 filename - return SaveInFormat(myPlot, width, height, Filepath, filename, Format); + // Set margins settings + if (AreaOrientation == Orientations.Horizontal) + { + myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); } else { - throw new ArgumentException("Error: Values and labels must be equal."); + myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); } + + // Set filepath + + string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); + + // Set filename + return SaveInFormat(myPlot, width, height, Filepath, filename, Format); } } } From b6d5db3e30855383a514f1701a22bb0e829fc9ac Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 25 Feb 2026 16:18:49 -0400 Subject: [PATCH 11/62] Add validation for category names length in StackedBarChart --- Sources/StackedBarChart.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 0d7f873..1a43f5c 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -82,6 +82,10 @@ public object Chart(List values, string[] labels, string[] categoryNam // assign values and colors to each bar if (values.Count > 0 && values[0].Length > 1) { + if (values .Count != categoryNames.Length) + { + throw new ArgumentException("Error: Values and category names must be equal."); + } for (int x = 0; x < values.Count; x++) { double nextBarBase = 0; @@ -105,8 +109,10 @@ public object Chart(List values, string[] labels, string[] categoryNam } else { - // Set a moderate bar width when there is only one element to prevent it from filling the entire chart area - // double barSize = values.Count == 1 ? 5.0 : 8.0; + if (values.Count != categoryNames.Length) + { + throw new ArgumentException("Error: Values and category names must be equal."); + } double nextBarBase = 0; for (int x = 0; x < values.Count; x++) { From b2db18fcf2f10b17850710c34223022618b99c8c Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Wed, 25 Feb 2026 21:07:25 -0400 Subject: [PATCH 12/62] Add validation for labels in StackedBarChart and create a Todo list for future enhancements --- Sources/StackedBarChart.cs | 10 +++++++++- Todo.md | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Todo.md diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 1a43f5c..c025369 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -82,10 +82,14 @@ public object Chart(List values, string[] labels, string[] categoryNam // assign values and colors to each bar if (values.Count > 0 && values[0].Length > 1) { - if (values .Count != categoryNames.Length) + if (values.Count != categoryNames.Length) { throw new ArgumentException("Error: Values and category names must be equal."); } + if (values.Count != labels.Length) + { + throw new ArgumentException("Error: Values and labels must be equal."); + } for (int x = 0; x < values.Count; x++) { double nextBarBase = 0; @@ -113,6 +117,10 @@ public object Chart(List values, string[] labels, string[] categoryNam { throw new ArgumentException("Error: Values and category names must be equal."); } + if (values[0].Length != labels.Length) + { + throw new ArgumentException("Error: Values and labels must be equal."); + } double nextBarBase = 0; for (int x = 0; x < values.Count; x++) { diff --git a/Todo.md b/Todo.md new file mode 100644 index 0000000..3b140b2 --- /dev/null +++ b/Todo.md @@ -0,0 +1,19 @@ +- [ ] Add option to set background color of the chart area + +```ScottPlot.Plot myPlot = new(); +// setup a plot with sample data +myPlot.Add.Signal(Generate.Sin(51)); +myPlot.Add.Signal(Generate.Cos(51)); +myPlot.XLabel("Horizontal Axis"); +myPlot.YLabel("Vertical Axis"); + +// some items must be styled directly +myPlot.FigureBackground.Color = Colors.Navy; +myPlot.DataBackground.Color = Colors.Navy.Darken(0.1); +myPlot.Grid.MajorLineColor = Colors.Navy.Lighten(0.1); + +// some items have helper methods to configure multiple properties at once +myPlot.Axes.Color(Colors.Navy.Lighten(0.8)); + +myPlot.SavePng("demo.png", 400, 300); +``` From c116d83dd466f431d50abdab5f5999c55cd5687f Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 26 Feb 2026 23:36:51 -0400 Subject: [PATCH 13/62] Refactor chart initialization and enhance axis limit handling for single value scenarios --- Sources/AsBuiltReportChart.cs | 8 ++++---- Sources/BarChart.cs | 14 +++++++++++++- Sources/StackedBarChart.cs | 16 ++++++++++++++-- Todo.md | 1 + 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/Sources/AsBuiltReportChart.cs b/Sources/AsBuiltReportChart.cs index 562d1a3..ccb4361 100644 --- a/Sources/AsBuiltReportChart.cs +++ b/Sources/AsBuiltReportChart.cs @@ -6,8 +6,8 @@ public static class MainEntry { public static void Main(string[] args) { - double[] values = new double[] { 3, 2 }; - string[] labels = new string[] { "Cassa", "Carro" }; + double[] values = new double[] { 3}; + string[] labels = new string[] { "Cassa"}; Chart.EnableLegend = true; Chart.LegendFontColor = BasicColors.Black; @@ -31,7 +31,7 @@ public static void Main(string[] args) // Chart.ColorPalette = ColorPalettes.Dark; - Chart.AreaOrientation = Orientations.Horizontal; + Chart.AreaOrientation = Orientations.Vertical; Bar PieBar = new Bar(); Chart.Format = Formats.png; @@ -39,7 +39,7 @@ public static void Main(string[] args) Chart.InvertCustomColorPalette = true; Chart.CustomColorPalette = new string[] { "#DFF0D0", "#FFF4C7", "#FEDDD7", "#878787", "#77a898", "#5e9584", "#458370", "#2a715d", "#005f4b" }; - PieBar.Chart(values, labels, width: 600, height: 400, filename: "PieChartExample"); + PieBar.Chart(values, labels, width: 600, height: 600, filename: "PieChartExample"); } } } \ No newline at end of file diff --git a/Sources/BarChart.cs b/Sources/BarChart.cs index 66556eb..1a6ad8d 100644 --- a/Sources/BarChart.cs +++ b/Sources/BarChart.cs @@ -187,8 +187,20 @@ public object Chart(double[] values, string[] labels, string filename = "output" // Set margins settings myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); - // Set filepath + // Set axis limits if values are empty or contain only one value to prevent auto-scaling issues + if (values.Length == 1) + { + if (AreaOrientation == Orientations.Horizontal) + { + myPlot.Axes.SetLimits(0, 0, -2, 2); + } + else + { + myPlot.Axes.SetLimits(-2, 2, 0, 0); + } + } + // Set filepath string Filepath = _outputFolderPath ?? System.IO.Directory.GetCurrentDirectory(); // Save Plot diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index c025369..15426ac 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -121,6 +121,7 @@ public object Chart(List values, string[] labels, string[] categoryNam { throw new ArgumentException("Error: Values and labels must be equal."); } + double nextBarBase = 0; for (int x = 0; x < values.Count; x++) { @@ -134,7 +135,6 @@ public object Chart(List values, string[] labels, string[] categoryNam FillColor = colorPalette.GetColor(x), Label = $"{values[x][0]}", CenterLabel = true, - // Size = barSize, }); nextBarBase += values[x][0]; } @@ -257,8 +257,20 @@ public object Chart(List values, string[] labels, string[] categoryNam myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); } - // Set filepath + // Set axis limits if values are empty or contain only one value to prevent auto-scaling issues + if (!(values.Count > 0 && values[0].Length > 1)) + { + if (AreaOrientation == Orientations.Horizontal) + { + myPlot.Axes.SetLimits(0, 0, -2, 2); + } + else + { + myPlot.Axes.SetLimits(-2, 2, 0, 0); + } + } + // Set filepath string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); // Set filename diff --git a/Todo.md b/Todo.md index 3b140b2..2afc45e 100644 --- a/Todo.md +++ b/Todo.md @@ -1,4 +1,5 @@ - [ ] Add option to set background color of the chart area + - [ ] https://scottplot.net/cookbook/5/Styling/BackgroundColors/ ```ScottPlot.Plot myPlot = new(); // setup a plot with sample data From 471a9646f11b6e7c564049fe12528ba33cfcad74 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sun, 1 Mar 2026 09:53:00 -0400 Subject: [PATCH 14/62] Add new SignalChart class file --- Sources/SignalChart.cs | 1 + 1 file changed, 1 insertion(+) create mode 100644 Sources/SignalChart.cs diff --git a/Sources/SignalChart.cs b/Sources/SignalChart.cs new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/Sources/SignalChart.cs @@ -0,0 +1 @@ + \ No newline at end of file From f38c51a6b1b80243b77f855adf20a96c772b8795 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:03:52 -0400 Subject: [PATCH 15/62] Add Signal chart support via New-SignalChart cmdlet (#13) * Initial plan * Add Signal chart support with New-SignalChart cmdlet Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> * Add New-SignalChart to FunctionsToExport in module manifest --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> Co-authored-by: Jonathan Colon --- AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 | 2 +- Sources/PowerShell/SignalChartPwsh.cs | 229 ++++++++++++++++++ Sources/SignalChart.cs | 136 +++++++++++ Tests/AsBuiltReport.Chart.Functions.Tests.ps1 | 27 +++ 4 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 Sources/PowerShell/SignalChartPwsh.cs create mode 100644 Sources/SignalChart.cs diff --git a/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 b/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 index e24a408..c0fbbeb 100644 --- a/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 +++ b/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 @@ -69,7 +69,7 @@ # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = 'New-PieChart', 'New-BarChart', 'New-StackedBarChart' + FunctionsToExport = 'New-PieChart', 'New-BarChart', 'New-StackedBarChart', 'New-SignalChart' # 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/Sources/PowerShell/SignalChartPwsh.cs b/Sources/PowerShell/SignalChartPwsh.cs new file mode 100644 index 0000000..529e86b --- /dev/null +++ b/Sources/PowerShell/SignalChartPwsh.cs @@ -0,0 +1,229 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Management.Automation; + +namespace AsBuiltReportChart.PowerShell +{ + [Cmdlet(VerbsCommon.New, "SignalChart")] + public class NewSignalChartCommand : Cmdlet + { + // Declare the parameters for the cmdlet. + [Parameter(Mandatory = false, HelpMessage = "Output filename for the chart. If not specified, a random token will be generated.")] + public string Filename { get; set; } = Chart.GenerateToken(8); + + [Parameter(Mandatory = true, HelpMessage = "List of double arrays representing each signal line to plot.")] + public List Values { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Array of labels for each signal line (used in the legend).")] + public string[] Labels { get; set; } + + // Title settings + [Parameter(Mandatory = true, HelpMessage = "Title text for the chart.")] + public string Title { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Make the title font bold.")] + public SwitchParameter TitleFontBold { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Font size for the title in points.")] + public int TitleFontSize { get; set; } = 14; + + [Parameter(Mandatory = false, HelpMessage = "Font color for the title.")] + public BasicColors TitleFontColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Enable the legend on the chart.")] + public SwitchParameter EnableLegend { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Orientation of the legend (Horizontal or Vertical).")] + public Enums.Orientations LegendOrientation { get; set; } = Enums.Orientations.Vertical; + + [Parameter(Mandatory = false, HelpMessage = "Alignment position of the legend on the chart.")] + public Enums.Alignments LegendAlignment { get; set; } = Enums.Alignments.UpperRight; + + [Parameter(Mandatory = false, HelpMessage = "Font size for the legend text in points.")] + public int LegendFontSize { get; set; } = 14; + + [Parameter(Mandatory = false, HelpMessage = "Make the legend font bold.")] + public SwitchParameter LegendBold { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Font color for the legend text.")] + public BasicColors LegendFontColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Border style for the legend box.")] + public Enums.BorderStyles LegendBorderStyle { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Border size for the legend box in pixels.")] + public int LegendBorderSize { get; set; } = 1; + + [Parameter(Mandatory = false, HelpMessage = "Border color for the legend box.")] + public BasicColors LegendBorderColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = true, HelpMessage = "Output format for the chart (PNG, JPEG, etc.).")] + public Formats Format { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Border color for the chart area.")] + public BasicColors ChartBorderColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Border size for the chart area in pixels.")] + public int ChartBorderSize { get; set; } = 1; + + [Parameter(Mandatory = false, HelpMessage = "Border style for the chart area.")] + public Enums.BorderStyles ChartBorderStyle { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Enable border around the chart area.")] + public SwitchParameter EnableChartBorder { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Color palette preset to use for the chart.")] + public Enums.ColorPalettes ColorPalette { get; set; } = Enums.ColorPalettes.Category10; + + [Parameter(Mandatory = false, HelpMessage = "Enable custom color palette for the chart.")] + 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 codes for the signal lines.")] + public string[] CustomColorPalette { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Font name to use for all text in the chart.")] + public string FontName { get; set; } = "Arial"; + + // Label Font settings + [Parameter(Mandatory = false, HelpMessage = "Font size for axis labels in points.")] + public int LabelFontSize { get; set; } = 14; + + [Parameter(Mandatory = false, HelpMessage = "Font color for axis labels.")] + public BasicColors LabelFontColor { get; set; } = BasicColors.Black; + + [Parameter(Mandatory = false, HelpMessage = "Make axis label fonts bold.")] + public SwitchParameter LabelBold { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Label text for the Y-axis.")] + public string LabelYAxis { get; set; } = "Count"; + + [Parameter(Mandatory = false, HelpMessage = "Label text for the X-axis.")] + public string LabelXAxis { get; set; } = "Values"; + + // Signal-specific settings + [Parameter(Mandatory = false, HelpMessage = "X-axis offset for the signal data as an OADate value (use (Get-Date '2024-01-01').ToOADate() to convert from a DateTime).")] + public double XOffset { get; set; } = 0; + + [Parameter(Mandatory = false, HelpMessage = "Period (interval) between each data point. Defaults to 1.0.")] + public double Period { get; set; } = 1.0; + + [Parameter(Mandatory = false, HelpMessage = "Display DateTime ticks on the bottom X axis.")] + public SwitchParameter DateTimeTicksBottom { get; set; } + + // Axes margins + [Parameter(Mandatory = false, HelpMessage = "Top margin for the chart area as a fraction (0-1).")] + public double AxesMarginsTop { get; set; } = 0.05; + + [Parameter(Mandatory = false, HelpMessage = "Bottom margin for the chart area as a fraction (0-1).")] + public double AxesMarginsDown { get; set; } = 0.05; + + [Parameter(Mandatory = false, HelpMessage = "Left margin for the chart area as a fraction (0-1).")] + public double AxesMarginsLeft { get; set; } = 0.05; + + [Parameter(Mandatory = false, HelpMessage = "Right margin for the chart area as a fraction (0-1).")] + public double AxesMarginsRight { get; set; } = 0.05; + + // Set chart Size WxH + [Parameter(Mandatory = false, HelpMessage = "Width of the output chart in pixels. Defaults to 400.")] + public int Width { get; set; } = 400; + + [Parameter(Mandatory = false, HelpMessage = "Height of the output chart in pixels. Defaults to 300.")] + public int Height { get; set; } = 300; + + // Set OutputFolderPath + [Parameter(Mandatory = false, HelpMessage = "Output folder path where the chart will be saved.")] + public string OutputFolderPath { get; set; } = Directory.GetCurrentDirectory(); + + protected override void ProcessRecord() + { + if (Values != null) + { + if (EnableLegend) + { + Chart.EnableLegend = EnableLegend; + // Legend box settings + Chart.LegendOrientation = LegendOrientation; + Chart.LegendAlignment = LegendAlignment; + + // Legend font settings + Chart.LegendFontSize = LegendFontSize; + Chart.LegendFontColor = LegendFontColor; + Chart.LegendBold = LegendBold; + // 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 Exception("EnableCustomColorPalette requires CustomColorPalette to be set."); + } + } + else + { + Chart.ColorPalette = ColorPalette; + } + + // Title settings + if (Title != null) + { + Chart.Title = Title; + Chart.TitleFontBold = TitleFontBold; + Chart.TitleFontSize = TitleFontSize; + Chart.TitleFontColor = TitleFontColor; + } + + // Font Settings + Chart.FontName = FontName; + Chart.LabelFontSize = LabelFontSize; + Chart.LabelFontColor = LabelFontColor; + Chart.LabelBold = LabelBold; + + // Set font for the X and Y axis labels + Chart.LabelXAxis = LabelXAxis; + Chart.LabelYAxis = LabelYAxis; + + // Axes margins + Chart.AxesMarginsTop = AxesMarginsTop; + Chart.AxesMarginsDown = AxesMarginsDown; + Chart.AxesMarginsLeft = AxesMarginsLeft; + Chart.AxesMarginsRight = AxesMarginsRight; + + // Set file directory save path + Chart.OutputFolderPath = OutputFolderPath; + + Chart.Format = Format; + SignalChart mySignalChart = new SignalChart(); + WriteObject(mySignalChart.Chart(Values, Labels, XOffset, Period, DateTimeTicksBottom, Filename, Width, Height)); + } + else + { + WriteObject("Values parameter cannot be null or empty."); + } + } + } +} diff --git a/Sources/SignalChart.cs b/Sources/SignalChart.cs new file mode 100644 index 0000000..afac786 --- /dev/null +++ b/Sources/SignalChart.cs @@ -0,0 +1,136 @@ +using ScottPlot; +using System; +using System.Collections.Generic; +using System.IO; +using AsBuiltReportChart.Enums; + +namespace AsBuiltReportChart +{ + internal class SignalChart : Chart + { + public object Chart(List values, string[] labels, double xOffset = 0, double period = 1.0, bool dateTimeTicksBottom = false, string filename = "output", int width = 400, int height = 300) + { + if (values == null || values.Count == 0) + { + throw new ArgumentException("Error: Values cannot be null or empty."); + } + + Plot myPlot = new Plot(); + + if (EnableCustomColorPalette) + { + if (_customColorPalette != null && _customColorPalette.Length > 0) + { + myPlot.Add.Palette = colorPalette = new ScottPlot.Palettes.Custom(_customColorPalette); + } + else + { + throw new Exception("CustomColorPalette is empty. Please provide valid color values."); + } + } + else + { + if (colorPalette != null) + { + myPlot.Add.Palette = colorPalette; + } + } + + // Add each signal line + for (int i = 0; i < values.Count; i++) + { + var sig = myPlot.Add.Signal(values[i]); + sig.Data.XOffset = xOffset; + sig.Data.Period = period; + + if (colorPalette != null) + { + sig.Color = colorPalette.GetColor(i); + } + + if (labels != null && i < labels.Length && EnableLegend) + { + sig.LegendText = labels[i]; + } + } + + // Enable DateTime ticks on the X axis if requested + if (dateTimeTicksBottom) + { + myPlot.Axes.DateTimeTicksBottom(); + } + + // Set X and Y axis label settings + myPlot.Axes.Bottom.Label.Text = LabelXAxis; + myPlot.Axes.Bottom.Label.FontSize = LabelFontSize; + myPlot.Axes.Bottom.Label.ForeColor = GetDrawingColor(LabelFontColor); + myPlot.Axes.Bottom.Label.FontName = FontName; + myPlot.Axes.Bottom.Label.Bold = LabelBold; + + myPlot.Axes.Bottom.TickLabelStyle.FontSize = LabelFontSize; + myPlot.Axes.Bottom.TickLabelStyle.ForeColor = GetDrawingColor(LabelFontColor); + myPlot.Axes.Bottom.TickLabelStyle.FontName = FontName; + + myPlot.Axes.Left.Label.Text = LabelYAxis; + myPlot.Axes.Left.Label.FontSize = LabelFontSize; + myPlot.Axes.Left.Label.ForeColor = GetDrawingColor(LabelFontColor); + myPlot.Axes.Left.Label.FontName = FontName; + myPlot.Axes.Left.Label.Bold = LabelBold; + + myPlot.Axes.Left.TickLabelStyle.FontSize = LabelFontSize; + myPlot.Axes.Left.TickLabelStyle.ForeColor = GetDrawingColor(LabelFontColor); + myPlot.Axes.Left.TickLabelStyle.FontName = FontName; + + // Hide unnecessary plot components + myPlot.HideGrid(); + myPlot.Axes.Top.IsVisible = false; + myPlot.Axes.Right.IsVisible = false; + + if (EnableLegend) + { + myPlot.ShowLegend(); + + // 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 filepath + string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); + + // Save Plot + return SaveInFormat(myPlot, width, height, Filepath, filename, Format); + } + } +} diff --git a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 index c946728..ead290d 100644 --- a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 +++ b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 @@ -17,6 +17,9 @@ Describe 'AsBuiltReport.Chart Exported Functions' { It 'Should export New-StackedBarChart' { Get-Command -Module AsBuiltReport.Chart -Name New-StackedBarChart | Should -Not -BeNullOrEmpty } + It 'Should export New-SignalChart' { + Get-Command -Module AsBuiltReport.Chart -Name New-SignalChart | Should -Not -BeNullOrEmpty + } Context 'New-PieChart' { It 'Should run without error with sample input' { @@ -79,4 +82,28 @@ Describe 'AsBuiltReport.Chart Exported Functions' { Test-Path $result | Should -BeTrue } } + + 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 + } + It 'Should return a file path as output' { + $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 + } + It 'Should run without error with multiple signal lines' { + { New-SignalChart -Title 'Test' -Values @(@(1, 2, 3), @(4, 5, 6)) -Labels @('Line1', 'Line2') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + 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 + } + It 'Should throw error for missing mandatory parameters' { + { New-SignalChart } | Should -Throw + } + It 'Should throw error when Values is null or empty' { + { New-SignalChart -Title 'Test' -Values $null -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw + } + } } From 3c770b9134425c0499195060e22ded3776e2b4ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:05:55 +0000 Subject: [PATCH 16/62] Initial plan From 1a172fa97078de035f1f0e51f71d60fc498b9403 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:19:03 +0000 Subject: [PATCH 17/62] Add scatter mode support to New-SignalChart cmdlet Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> --- Sources/PowerShell/SignalChartPwsh.cs | 5 +- Sources/SignalChart.cs | 57 +++++++++++++++---- Tests/AsBuiltReport.Chart.Functions.Tests.ps1 | 29 ++++++++++ 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/Sources/PowerShell/SignalChartPwsh.cs b/Sources/PowerShell/SignalChartPwsh.cs index 529e86b..0448a1b 100644 --- a/Sources/PowerShell/SignalChartPwsh.cs +++ b/Sources/PowerShell/SignalChartPwsh.cs @@ -108,6 +108,9 @@ public class NewSignalChartCommand : Cmdlet [Parameter(Mandatory = false, HelpMessage = "X-axis offset for the signal data as an OADate value (use (Get-Date '2024-01-01').ToOADate() to convert from a DateTime).")] public double XOffset { get; set; } = 0; + [Parameter(Mandatory = false, HelpMessage = "List of double arrays representing the X values for each scatter line. When provided, scatter mode is used instead of signal mode. Use OADate values for DateTime X axes (e.g. (Get-Date '2024-01-01').ToOADate()).")] + public List ScatterXValues { get; set; } + [Parameter(Mandatory = false, HelpMessage = "Period (interval) between each data point. Defaults to 1.0.")] public double Period { get; set; } = 1.0; @@ -218,7 +221,7 @@ protected override void ProcessRecord() Chart.Format = Format; SignalChart mySignalChart = new SignalChart(); - WriteObject(mySignalChart.Chart(Values, Labels, XOffset, Period, DateTimeTicksBottom, Filename, Width, Height)); + WriteObject(mySignalChart.Chart(Values, Labels, XOffset, Period, DateTimeTicksBottom, Filename, Width, Height, ScatterXValues)); } else { diff --git a/Sources/SignalChart.cs b/Sources/SignalChart.cs index afac786..d86d738 100644 --- a/Sources/SignalChart.cs +++ b/Sources/SignalChart.cs @@ -8,13 +8,29 @@ namespace AsBuiltReportChart { internal class SignalChart : Chart { - public object Chart(List values, string[] labels, double xOffset = 0, double period = 1.0, bool dateTimeTicksBottom = false, string filename = "output", int width = 400, int height = 300) + public object Chart(List values, string[] labels, double xOffset = 0, double period = 1.0, bool dateTimeTicksBottom = false, string filename = "output", int width = 400, int height = 300, List xValues = null) { if (values == null || values.Count == 0) { throw new ArgumentException("Error: Values cannot be null or empty."); } + if (xValues != null) + { + if (xValues.Count != values.Count) + { + throw new ArgumentException("Error: XValues and Values must have the same number of arrays."); + } + + for (int i = 0; i < xValues.Count; i++) + { + if (xValues[i].Length != values[i].Length) + { + throw new ArgumentException($"Error: XValues and Values at index {i} must have the same number of elements."); + } + } + } + Plot myPlot = new Plot(); if (EnableCustomColorPalette) @@ -36,21 +52,40 @@ public object Chart(List values, string[] labels, double xOffset = 0, } } - // Add each signal line + // Add each signal or scatter line for (int i = 0; i < values.Count; i++) { - var sig = myPlot.Add.Signal(values[i]); - sig.Data.XOffset = xOffset; - sig.Data.Period = period; - - if (colorPalette != null) + if (xValues != null) { - sig.Color = colorPalette.GetColor(i); + // Scatter mode: explicit X and Y values + var scatter = myPlot.Add.Scatter(xValues[i], values[i]); + + if (colorPalette != null) + { + scatter.Color = colorPalette.GetColor(i); + } + + if (labels != null && i < labels.Length && EnableLegend) + { + scatter.LegendText = labels[i]; + } } - - if (labels != null && i < labels.Length && EnableLegend) + else { - sig.LegendText = labels[i]; + // Signal mode: Y values only with uniform X spacing + var sig = myPlot.Add.Signal(values[i]); + sig.Data.XOffset = xOffset; + sig.Data.Period = period; + + if (colorPalette != null) + { + sig.Color = colorPalette.GetColor(i); + } + + if (labels != null && i < labels.Length && EnableLegend) + { + sig.LegendText = labels[i]; + } } } diff --git a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 index ead290d..87a74a2 100644 --- a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 +++ b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 @@ -99,6 +99,35 @@ Describe 'AsBuiltReport.Chart Exported Functions' { $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 } + 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 + } + 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 + $result | Should -BeOfType 'System.IO.FileSystemInfo' + Test-Path $result | Should -BeTrue + } + It 'Should run without error in scatter mode with DateTime X values' { + $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 + } | Should -Not -Throw + } + It 'Should run without error in scatter mode with multiple lines' { + $xValues = @(@(1.0, 2.0, 3.0), @(1.0, 2.0, 3.0)) + { 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)) + { 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." + } It 'Should throw error for missing mandatory parameters' { { New-SignalChart } | Should -Throw } From c8cf4a46226d9aa60751ecced16c9863bf18dcba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:35:10 +0000 Subject: [PATCH 18/62] Initial plan From 7b24248473b44b2f3f7ca2c1b6d081edc7d09255 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:45:49 +0000 Subject: [PATCH 19/62] Add ONTAP-NAS day data tests for Signal chart with Scatter and DateTimeTicksBottom Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> --- Tests/AsBuiltReport.Chart.Functions.Tests.ps1 | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 index 87a74a2..212d76e 100644 --- a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 +++ b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 @@ -134,5 +134,45 @@ Describe 'AsBuiltReport.Chart Exported Functions' { It 'Should throw error when Values is null or empty' { { New-SignalChart -Title 'Test' -Values $null -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw } + Context 'ONTAP-NAS day data (Scatter + DateTimeTicksBottom)' { + BeforeAll { + # Simulate ONTAP-NAS day data: 24 hourly data points for multiple NFS metrics + $baseDate = (Get-Date '2024-01-01').ToOADate() + $script:OntapXArr = [double[]](0..23 | ForEach-Object { $baseDate + ($_ / 24.0) }) + $script:OntapNfsRead = [double[]]@(10.2, 12.5, 8.7, 6.3, 5.1, 7.4, 11.8, 15.6, 18.2, 20.1, 22.4, 19.8, 17.3, 16.5, 18.9, 21.2, 23.6, 20.8, 17.4, 14.2, 12.1, 10.5, 9.3, 8.8) + $script:OntapNfsWrite = [double[]]@(5.1, 6.2, 4.3, 3.1, 2.5, 3.7, 5.9, 7.8, 9.1, 10.0, 11.2, 9.9, 8.6, 8.2, 9.4, 10.6, 11.8, 10.4, 8.7, 7.1, 6.0, 5.2, 4.6, 4.4) + } + It 'Should run without error with ONTAP-NAS day data in scatter mode with DateTimeTicksBottom' { + { + New-SignalChart ` + -Title 'ONTAP-NAS NFS Throughput (Day)' ` + -Values @($script:OntapNfsRead, $script:OntapNfsWrite) ` + -ScatterXValues @($script:OntapXArr, $script:OntapXArr) ` + -Labels @('NFS Read', 'NFS Write') ` + -LabelXAxis 'Time' ` + -LabelYAxis 'Throughput (MB/s)' ` + -DateTimeTicksBottom ` + -EnableLegend ` + -Format 'png' ` + -OutputFolderPath $TestDrive + } | Should -Not -Throw + } + It 'Should return a file path for ONTAP-NAS day data chart' { + $result = New-SignalChart ` + -Title 'ONTAP-NAS NFS Throughput (Day)' ` + -Values @($script:OntapNfsRead, $script:OntapNfsWrite) ` + -ScatterXValues @($script:OntapXArr, $script:OntapXArr) ` + -Labels @('NFS Read', 'NFS Write') ` + -LabelXAxis 'Time' ` + -LabelYAxis 'Throughput (MB/s)' ` + -DateTimeTicksBottom ` + -EnableLegend ` + -Format 'png' ` + -OutputFolderPath $TestDrive + + $result | Should -BeOfType 'System.IO.FileSystemInfo' + Test-Path $result | Should -BeTrue + } + } } } From a39dafecbefc9af560c843ba26fd15e3b60d60fe Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sun, 1 Mar 2026 22:09:23 -0400 Subject: [PATCH 20/62] Update README.md with new links and module info --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7dc9660..aadec70 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ This report is compatible with the following PowerShell versions; ## 🗺️ Language Support -The AsBuiltReport Chart As Built Report supports the following languages; +The AsBuiltReport Chart supports the following languages; - English (US) (Default) @@ -111,3 +111,4 @@ New-StackedBarChart -Title 'Test' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -L + From 6e55062124420baafb2972634507d336f10fc783 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Fri, 6 Mar 2026 22:48:58 -0400 Subject: [PATCH 21/62] Update CHANGELOG with new version and features --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aed89a..eb1c071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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.0] - 2026-03-?? + +### Added + +- Add Signal chart support + ## [0.2.0] - 2026-02-20 ### Added @@ -19,4 +25,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Initial release of AsBuiltReport Chart, providing basic charting capabilities for AsBuiltReport data visualization \ No newline at end of file + +- Initial release of AsBuiltReport Chart, providing basic charting capabilities for AsBuiltReport data visualization From 119c2be95f4737a0c29be49664aa1c4e7bdf00d4 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Fri, 6 Mar 2026 22:49:16 -0400 Subject: [PATCH 22/62] Update module version to 0.3.0 in manifest --- AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 b/AsBuiltReport.Chart/AsBuiltReport.Chart.psd1 index c0fbbeb..e809b78 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.2.0' + ModuleVersion = '0.3.0' # Supported PSEditions # CompatiblePSEditions = @() @@ -127,3 +127,4 @@ + From 48716dfbd82824c502b7bda023819facbdf40962 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:06:22 +0000 Subject: [PATCH 23/62] Initial plan From 8c95dee97077315df2ef3cf7d8af2740dfc69642 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:13:21 +0000 Subject: [PATCH 24/62] Improvements: fix ColorblindFriendlyDark bug, security, performance, background color feature Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> --- Sources/BarChart.cs | 10 ++++++++ Sources/Chart.cs | 30 +++++++++++------------ Sources/PieChart.cs | 10 ++++++++ Sources/PowerShell/BarChartPwsh.cs | 10 ++++++++ Sources/PowerShell/PieChartPwsh.cs | 10 ++++++++ Sources/PowerShell/SignalChartPwsh.cs | 10 ++++++++ Sources/PowerShell/StackedBarChartPwsh.cs | 10 ++++++++ Sources/SignalChart.cs | 10 ++++++++ Sources/StackedBarChart.cs | 11 ++++++--- Todo.md | 4 +-- 10 files changed, 93 insertions(+), 22 deletions(-) diff --git a/Sources/BarChart.cs b/Sources/BarChart.cs index 1a6ad8d..fec6bf8 100644 --- a/Sources/BarChart.cs +++ b/Sources/BarChart.cs @@ -187,6 +187,16 @@ public object Chart(double[] values, string[] labels, string filename = "output" // 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 axis limits if values are empty or contain only one value to prevent auto-scaling issues if (values.Length == 1) { diff --git a/Sources/Chart.cs b/Sources/Chart.cs index 086fc62..2d7c4b4 100644 --- a/Sources/Chart.cs +++ b/Sources/Chart.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.IO; +using System.Security.Cryptography; using System.Text.RegularExpressions; using AsBuiltReportChart.Enums; @@ -101,6 +102,7 @@ public static double AreaExplodeFraction { ColorPalettes.Aurora, new ScottPlot.Palettes.Aurora() }, { ColorPalettes.Building, new ScottPlot.Palettes.Building() }, { ColorPalettes.ColorblindFriendly, new ScottPlot.Palettes.ColorblindFriendly() }, + { ColorPalettes.ColorblindFriendlyDark, new ScottPlot.Palettes.ColorblindFriendlyDark() }, { ColorPalettes.Dark, new ScottPlot.Palettes.Dark() }, { ColorPalettes.DarkPastel, new ScottPlot.Palettes.DarkPastel() }, { ColorPalettes.Frost, new ScottPlot.Palettes.Frost() }, @@ -138,7 +140,7 @@ public static ColorPalettes? ColorPalette internal static string[] _customColorPalette; public static string[] CustomColorPalette { - get => _customColorPalette ?? new string[0]; + get => _customColorPalette ?? Array.Empty(); set { if (value != null && value.Length > 0) @@ -196,11 +198,10 @@ public static string OutputFolderPath public static bool IsValidHexColor(string hexCode) { // Regex for #RGB, #RRGGBB, #RGBA, or #RRGGBBAA formats (case-insensitive) - var regex = MyRegex(); - return regex.IsMatch(hexCode); + return HexColorRegex.IsMatch(hexCode); } - private static Regex MyRegex() => new Regex("^#([A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$"); + private static readonly Regex HexColorRegex = new Regex("^#([A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$", RegexOptions.Compiled); internal static readonly IReadOnlyDictionary LegendBorderStyleMap = new Dictionary() { @@ -319,6 +320,10 @@ public static double AxesMarginsRight } } + // Chart background color settings (All Charts) + public static BasicColors? FigureBackgroundColor { get; set; } + public static BasicColors? DataBackgroundColor { get; set; } + public static Color GetDrawingColor(BasicColors color) { return ColorMap[color]; @@ -326,8 +331,10 @@ public static Color GetDrawingColor(BasicColors color) public static string GenerateToken(Byte length) { var bytes = new byte[length]; - var rnd = new Random(); - rnd.NextBytes(bytes); + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(bytes); + } return Convert.ToBase64String(bytes).Replace("=", "").Replace("+", "").Replace("/", ""); } public static object SaveInFormat(Plot plot, int width, int height, string filepath, string filename, Formats Format) @@ -400,16 +407,7 @@ public static object SaveInFormat(Plot plot, int width, int height, string filep throw new ArgumentException("Error: Unable to Export Chart Exception"); } default: - plot.SavePng(Path.Combine(filepath, $"{filename}.png"), width, height); - if (File.Exists(Path.Combine(filepath, $"{filename}.png"))) - { - FileInfo fileInfo = new FileInfo(Path.Combine(filepath, $"{filename}.png")); - return fileInfo; - } - else - { - throw new ArgumentException("Error: Unable to Export Chart Exception"); - } + throw new ArgumentException($"Error: Unsupported format '{Format}'."); } } } diff --git a/Sources/PieChart.cs b/Sources/PieChart.cs index c87fbce..64d135d 100644 --- a/Sources/PieChart.cs +++ b/Sources/PieChart.cs @@ -98,6 +98,16 @@ public object Chart(double[] values, string[] labels, string filename = "output" // 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 filetpath to save string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); diff --git a/Sources/PowerShell/BarChartPwsh.cs b/Sources/PowerShell/BarChartPwsh.cs index 13cf3c4..0c3105b 100644 --- a/Sources/PowerShell/BarChartPwsh.cs +++ b/Sources/PowerShell/BarChartPwsh.cs @@ -121,6 +121,12 @@ public class NewBarChartCommand : Cmdlet [Parameter(Mandatory = false, HelpMessage = "Right margin for the chart area as a percentage (0-1).")] 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; } + // Set chart Size WxH [Parameter(Mandatory = false, HelpMessage = "Width of the output chart in pixels. Defaults to 400.")] public int Width { get; set; } = 400; @@ -210,6 +216,10 @@ protected override void ProcessRecord() Chart.AxesMarginsLeft = AxesMarginsLeft; Chart.AxesMarginsRight = AxesMarginsRight; + // Background color settings + Chart.FigureBackgroundColor = FigureBackgroundColor; + Chart.DataBackgroundColor = DataBackgroundColor; + // Set file directory save path Chart.OutputFolderPath = OutputFolderPath; diff --git a/Sources/PowerShell/PieChartPwsh.cs b/Sources/PowerShell/PieChartPwsh.cs index bd8588c..9b0abd6 100644 --- a/Sources/PowerShell/PieChartPwsh.cs +++ b/Sources/PowerShell/PieChartPwsh.cs @@ -115,6 +115,12 @@ public class NewPieChartCommand : Cmdlet [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; } + // Set chart Size WxH [Parameter(Mandatory = false, HelpMessage = "Width of the chart in pixels. Defaults to 400.")] public int Width { get; set; } = 400; @@ -200,6 +206,10 @@ protected override void ProcessRecord() Chart.LabelFontColor = LabelFontColor; Chart.LabelBold = LabelBold; + // Background color settings + Chart.FigureBackgroundColor = FigureBackgroundColor; + Chart.DataBackgroundColor = DataBackgroundColor; + // Set file directory save path Chart.OutputFolderPath = OutputFolderPath; diff --git a/Sources/PowerShell/SignalChartPwsh.cs b/Sources/PowerShell/SignalChartPwsh.cs index 0448a1b..0b7ddda 100644 --- a/Sources/PowerShell/SignalChartPwsh.cs +++ b/Sources/PowerShell/SignalChartPwsh.cs @@ -130,6 +130,12 @@ public class NewSignalChartCommand : Cmdlet [Parameter(Mandatory = false, HelpMessage = "Right margin for the chart area as a fraction (0-1).")] 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; } + // Set chart Size WxH [Parameter(Mandatory = false, HelpMessage = "Width of the output chart in pixels. Defaults to 400.")] public int Width { get; set; } = 400; @@ -216,6 +222,10 @@ protected override void ProcessRecord() Chart.AxesMarginsLeft = AxesMarginsLeft; Chart.AxesMarginsRight = AxesMarginsRight; + // Background color settings + Chart.FigureBackgroundColor = FigureBackgroundColor; + Chart.DataBackgroundColor = DataBackgroundColor; + // Set file directory save path Chart.OutputFolderPath = OutputFolderPath; diff --git a/Sources/PowerShell/StackedBarChartPwsh.cs b/Sources/PowerShell/StackedBarChartPwsh.cs index 099dbba..09e728c 100644 --- a/Sources/PowerShell/StackedBarChartPwsh.cs +++ b/Sources/PowerShell/StackedBarChartPwsh.cs @@ -125,6 +125,12 @@ public class NewStackedBarChartCommand : Cmdlet [Parameter(Mandatory = false, HelpMessage = "Set the right margin for the chart area axes.")] 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; } + // Set chart Size WxH [Parameter(Mandatory = false, HelpMessage = "Set the width of the chart in pixels.")] public int Width { get; set; } = 400; @@ -213,6 +219,10 @@ protected override void ProcessRecord() Chart.AxesMarginsLeft = AxesMarginsLeft; Chart.AxesMarginsRight = AxesMarginsRight; + // Background color settings + Chart.FigureBackgroundColor = FigureBackgroundColor; + Chart.DataBackgroundColor = DataBackgroundColor; + // Set file directory save path Chart.OutputFolderPath = OutputFolderPath; diff --git a/Sources/SignalChart.cs b/Sources/SignalChart.cs index d86d738..7d8fb1e 100644 --- a/Sources/SignalChart.cs +++ b/Sources/SignalChart.cs @@ -161,6 +161,16 @@ public object Chart(List values, string[] labels, double xOffset = 0, // 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 filepath string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 15426ac..d58b6d9 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -248,13 +248,16 @@ public object Chart(List values, string[] labels, string[] categoryNam } // Set margins settings - if (AreaOrientation == Orientations.Horizontal) + myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); + + // Set background colors + if (FigureBackgroundColor.HasValue) { - myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); + myPlot.FigureBackground.Color = GetDrawingColor(FigureBackgroundColor.Value); } - else + if (DataBackgroundColor.HasValue) { - myPlot.Axes.Margins(left: AxesMarginsLeft, right: AxesMarginsRight, bottom: AxesMarginsDown, top: AxesMarginsTop); + myPlot.DataBackground.Color = GetDrawingColor(DataBackgroundColor.Value); } // Set axis limits if values are empty or contain only one value to prevent auto-scaling issues diff --git a/Todo.md b/Todo.md index 2afc45e..ad6f254 100644 --- a/Todo.md +++ b/Todo.md @@ -1,5 +1,5 @@ -- [ ] Add option to set background color of the chart area - - [ ] https://scottplot.net/cookbook/5/Styling/BackgroundColors/ +- [x] Add option to set background color of the chart area + - [x] https://scottplot.net/cookbook/5/Styling/BackgroundColors/ ```ScottPlot.Plot myPlot = new(); // setup a plot with sample data From 3b054aaa176674c7885901dd5a5cc12f465f9b4d Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sun, 8 Mar 2026 16:14:24 -0400 Subject: [PATCH 25/62] Update CHANGELOG with new chart features Added support for setting the background color of the chart. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1c071..70366d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add Signal chart support +- Add support for setting the background color of the chart ## [0.2.0] - 2026-02-20 @@ -27,3 +28,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release of AsBuiltReport Chart, providing basic charting capabilities for AsBuiltReport data visualization + From 7f165336271f3da8bef42bf29d5e318f7dcbf487 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sun, 8 Mar 2026 16:15:13 -0400 Subject: [PATCH 26/62] Revise CHANGELOG for version 0.3.0 Updated the changelog to reflect version 0.3.0 changes. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70366d0..0903e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Signal chart support - Add support for setting the background color of the chart +### Changed + +Update module version to 0.3.0 + ## [0.2.0] - 2026-02-20 ### Added @@ -29,3 +33,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release of AsBuiltReport Chart, providing basic charting capabilities for AsBuiltReport data visualization + From 0cf328f461208e48db507baaf5bc406cf4056b83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 20:58:37 +0000 Subject: [PATCH 27/62] Initial plan From efb3d866ad930fa8027905a7a3f23cb77463fd4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:02:49 +0000 Subject: [PATCH 28/62] Add Examples directory with 9 PowerShell example scripts for all chart types Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> --- Examples/Example01.ps1 | 55 +++++++++++++++++++ Examples/Example02.ps1 | 98 ++++++++++++++++++++++++++++++++++ Examples/Example03.ps1 | 59 +++++++++++++++++++++ Examples/Example04.ps1 | 96 +++++++++++++++++++++++++++++++++ Examples/Example05.ps1 | 79 +++++++++++++++++++++++++++ Examples/Example06.ps1 | 111 ++++++++++++++++++++++++++++++++++++++ Examples/Example07.ps1 | 66 +++++++++++++++++++++++ Examples/Example08.ps1 | 83 +++++++++++++++++++++++++++++ Examples/Example09.ps1 | 118 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 765 insertions(+) create mode 100644 Examples/Example01.ps1 create mode 100644 Examples/Example02.ps1 create mode 100644 Examples/Example03.ps1 create mode 100644 Examples/Example04.ps1 create mode 100644 Examples/Example05.ps1 create mode 100644 Examples/Example06.ps1 create mode 100644 Examples/Example07.ps1 create mode 100644 Examples/Example08.ps1 create mode 100644 Examples/Example09.ps1 diff --git a/Examples/Example01.ps1 b/Examples/Example01.ps1 new file mode 100644 index 0000000..44137fb --- /dev/null +++ b/Examples/Example01.ps1 @@ -0,0 +1,55 @@ +<# + .SYNOPSIS + Example 01 - Basic Pie Chart + + .DESCRIPTION + This example demonstrates how to create a basic Pie Chart using the AsBuiltReport.Chart module. + The chart displays a simple breakdown of VM power states across a vSphere environment. +#> + +[CmdletBinding()] +param ( + [System.IO.FileInfo] $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 = 'VM Power States' +$Values = @(120, 35, 10) +$Labels = @('Powered On', 'Powered Off', 'Suspended') + +<# + The New-PieChart cmdlet generates the Pie 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. + -Filename : Name of the output file (without extension). +#> + +New-PieChart ` + -Title $ChartTitle ` + -Values $Values ` + -Labels $Labels ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Filename 'Example01-PieChart' diff --git a/Examples/Example02.ps1 b/Examples/Example02.ps1 new file mode 100644 index 0000000..07d5c1c --- /dev/null +++ b/Examples/Example02.ps1 @@ -0,0 +1,98 @@ +<# + .SYNOPSIS + Example 02 - Pie Chart with Legend, Custom Colors and Border + + .DESCRIPTION + This example demonstrates how to create a Pie 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.FileInfo] $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-PieChart cmdlet generates the Pie 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-PieChart ` + -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 'Example02-PieChart-Advanced' diff --git a/Examples/Example03.ps1 b/Examples/Example03.ps1 new file mode 100644 index 0000000..437b87c --- /dev/null +++ b/Examples/Example03.ps1 @@ -0,0 +1,59 @@ +<# + .SYNOPSIS + Example 03 - Basic Bar Chart + + .DESCRIPTION + This example demonstrates how to create a basic Bar Chart using the AsBuiltReport.Chart module. + The chart displays CPU utilization across a set of ESXi hosts. +#> + +[CmdletBinding()] +param ( + [System.IO.FileInfo] $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 = 'ESXi Host CPU Utilization (%)' +$Values = @(72, 45, 88, 61, 53) +$Labels = @('esxi-host-01', 'esxi-host-02', 'esxi-host-03', 'esxi-host-04', 'esxi-host-05') + +<# + The New-BarChart cmdlet generates the Bar Chart image. + + -Title : Sets the chart title displayed at the top of the image. + -Values : Array of numeric values, one per bar. + -Labels : Array of label strings corresponding to each bar. + -LabelXAxis : Label for the X-axis. + -LabelYAxis : Label for the Y-axis. + -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-BarChart ` + -Title $ChartTitle ` + -Values $Values ` + -Labels $Labels ` + -LabelXAxis 'Host' ` + -LabelYAxis 'CPU (%)' ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Filename 'Example03-BarChart' diff --git a/Examples/Example04.ps1 b/Examples/Example04.ps1 new file mode 100644 index 0000000..c312e60 --- /dev/null +++ b/Examples/Example04.ps1 @@ -0,0 +1,96 @@ +<# + .SYNOPSIS + Example 04 - Bar Chart with Advanced Options + + .DESCRIPTION + This example demonstrates how to create a Bar Chart with advanced options, including: + - Horizontal bar orientation + - Custom color palette + - Bold title and labels + - Custom axis labels and chart dimensions + - Chart border + + The chart displays memory utilization across a set of physical servers. +#> + +[CmdletBinding()] +param ( + [System.IO.FileInfo] $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 = 'Physical Server Memory Utilization (%)' +$Values = @(91, 67, 78, 55, 83, 44) +$Labels = @('srv-prod-01', 'srv-prod-02', 'srv-prod-03', 'srv-dev-01', 'srv-dev-02', 'srv-test-01') + +<# + A custom hex color palette can be used to match corporate branding or improve readability. + Each color corresponds to a bar in the order it appears in $Values. +#> + +$CustomColors = @('#E74C3C', '#E67E22', '#F1C40F', '#2ECC71', '#3498DB', '#9B59B6') + +<# + The New-BarChart cmdlet generates the Bar 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 bar. + -Labels : Array of label strings corresponding to each bar. + -LabelFontSize : Sets the font size of the bar labels. + -LabelBold : Renders the bar labels in bold. + -LabelXAxis : Label for the X-axis. + -LabelYAxis : Label for the Y-axis. + -AreaOrientation : Orientation of the bars (Horizontal or Vertical). + -AxesMarginsTop : Top margin for the chart area as a fraction (0-1). + -EnableChartBorder : Draws a border around the chart area. + -ChartBorderColor : Sets the border color. + -EnableCustomColorPalette : Enables use of the custom color palette. + -CustomColorPalette : Array of hex color strings for each bar. + -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-BarChart ` + -Title $ChartTitle ` + -TitleFontSize 16 ` + -TitleFontBold ` + -Values $Values ` + -Labels $Labels ` + -LabelFontSize 12 ` + -LabelBold ` + -LabelXAxis 'Memory (%)' ` + -LabelYAxis 'Server' ` + -AreaOrientation Horizontal ` + -AxesMarginsTop 1 ` + -EnableChartBorder ` + -ChartBorderColor Black ` + -EnableCustomColorPalette ` + -CustomColorPalette $CustomColors ` + -Width 700 ` + -Height 450 ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Filename 'Example04-BarChart-Advanced' diff --git a/Examples/Example05.ps1 b/Examples/Example05.ps1 new file mode 100644 index 0000000..ccc4cea --- /dev/null +++ b/Examples/Example05.ps1 @@ -0,0 +1,79 @@ +<# + .SYNOPSIS + Example 05 - Basic Stacked Bar Chart + + .DESCRIPTION + This example demonstrates how to create a basic Stacked Bar Chart using the AsBuiltReport.Chart module. + The chart displays datastore utilization (used vs. free space) for a set of vSphere datastores. +#> + +[CmdletBinding()] +param ( + [System.IO.FileInfo] $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. + + For a Stacked Bar Chart: + - $Values is an array of double arrays. Each inner array contains the values for one category + (stack segment) across all bars. + - $Labels contains the label for each bar (X-axis entries). + - $LegendCategories contains the label for each series (stack segments shown in the legend). + + In this example: + - Bar 1 = datastore-01: 800 GB Used, 200 GB Free + - Bar 2 = datastore-02: 600 GB Used, 400 GB Free + - Bar 3 = datastore-03: 1500 GB Used, 500 GB Free + - Bar 4 = datastore-04: 300 GB Used, 700 GB Free +#> + +$ChartTitle = 'Datastore Capacity (GB)' +$Labels = @('datastore-01', 'datastore-02', 'datastore-03', 'datastore-04') +$LegendCategories = @('Used Space', 'Free Space') + +# Each inner array represents one category's values across all bars. +$UsedSpace = [double[]]@(800, 600, 1500, 300) +$FreeSpace = [double[]]@(200, 400, 500, 700) +$Values = @($UsedSpace, $FreeSpace) + +<# + The New-StackedBarChart cmdlet generates the Stacked Bar Chart image. + + -Title : Sets the chart title displayed at the top of the image. + -Values : Array of double arrays. Each inner array is one stack segment across all bars. + -Labels : Array of label strings, one per bar (X-axis entries). + -LegendCategories : Array of category names shown in the legend (one per inner array in Values). + -EnableLegend : Enables the legend on the chart. + -LabelXAxis : Label for the X-axis. + -LabelYAxis : Label for the Y-axis. + -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-StackedBarChart ` + -Title $ChartTitle ` + -Values $Values ` + -Labels $Labels ` + -LegendCategories $LegendCategories ` + -EnableLegend ` + -LabelXAxis 'Datastore' ` + -LabelYAxis 'Capacity (GB)' ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Filename 'Example05-StackedBarChart' diff --git a/Examples/Example06.ps1 b/Examples/Example06.ps1 new file mode 100644 index 0000000..626d04d --- /dev/null +++ b/Examples/Example06.ps1 @@ -0,0 +1,111 @@ +<# + .SYNOPSIS + Example 06 - Stacked Bar Chart with Advanced Options + + .DESCRIPTION + This example demonstrates how to create a Stacked Bar Chart with advanced options, including: + - Custom color palette + - Horizontal legend alignment at the top of the chart + - Bold title with larger font + - Custom axis labels + - Larger chart dimensions + + The chart displays network traffic breakdown (Inbound, Outbound, Dropped) per network adapter. +#> + +[CmdletBinding()] +param ( + [System.IO.FileInfo] $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. + + For a Stacked Bar Chart: + - $Values is an array of double arrays. Each inner array contains the values for one category + (stack segment) across all bars. + - $Labels contains the label for each bar (X-axis entries). + - $LegendCategories contains the label for each series (stack segments shown in the legend). + + In this example each bar represents a network adapter, and each stack segment is a traffic type. +#> + +$ChartTitle = 'Network Adapter Traffic (Mbps)' +$Labels = @('vmnic0', 'vmnic1', 'vmnic2', 'vmnic3') +$LegendCategories = @('Inbound', 'Outbound', 'Dropped') + +# Each inner array represents one traffic category across all adapters. +$Inbound = [double[]]@(450, 320, 280, 510) +$Outbound = [double[]]@(380, 290, 210, 460) +$Dropped = [double[]]@(15, 5, 30, 8) +$Values = @($Inbound, $Outbound, $Dropped) + +<# + A custom hex color palette can be used to match corporate branding or improve readability. + Each color corresponds to a stack segment category in the order they appear in $LegendCategories. +#> + +$CustomColors = @('#3498DB', '#2ECC71', '#E74C3C') + +<# + The New-StackedBarChart cmdlet generates the Stacked Bar 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 double arrays, one per stack segment category. + -Labels : Array of label strings, one per bar (X-axis entries). + -LegendCategories : Array of category names shown in the legend. + -EnableLegend : Enables the legend on the chart. + -LegendOrientation : Sets legend layout direction (Vertical or Horizontal). + -LegendAlignment : Positions the legend (e.g. UpperCenter, UpperRight). + -LegendFontSize : Sets the legend text font size in points. + -LabelFontSize : Sets the font size of the axis labels. + -LabelXAxis : Label for the X-axis. + -LabelYAxis : Label for the Y-axis. + -AxesMarginsTop : Top margin for the chart area as a fraction (0-1). + -EnableCustomColorPalette : Enables use of the custom color palette. + -CustomColorPalette : Array of hex color strings, one per stack segment category. + -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-StackedBarChart ` + -Title $ChartTitle ` + -TitleFontSize 18 ` + -TitleFontBold ` + -Values $Values ` + -Labels $Labels ` + -LegendCategories $LegendCategories ` + -EnableLegend ` + -LegendOrientation Horizontal ` + -LegendAlignment UpperCenter ` + -LegendFontSize 12 ` + -LabelFontSize 13 ` + -LabelXAxis 'Network Adapter' ` + -LabelYAxis 'Traffic (Mbps)' ` + -AxesMarginsTop 1 ` + -EnableCustomColorPalette ` + -CustomColorPalette $CustomColors ` + -Width 700 ` + -Height 450 ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Filename 'Example06-StackedBarChart-Advanced' diff --git a/Examples/Example07.ps1 b/Examples/Example07.ps1 new file mode 100644 index 0000000..51da8cb --- /dev/null +++ b/Examples/Example07.ps1 @@ -0,0 +1,66 @@ +<# + .SYNOPSIS + Example 07 - Basic Signal Chart (Line Chart) + + .DESCRIPTION + This example demonstrates how to create a basic Signal Chart (line chart) using the + AsBuiltReport.Chart module. + + The Signal Chart is used to visualize sequential data points as a continuous line. In this + example, a single line representing hourly CPU utilization over a 24-hour period is plotted. +#> + +[CmdletBinding()] +param ( + [System.IO.FileInfo] $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. + + For a Signal Chart: + - $Values is an array of double arrays. Each inner array represents a single line/series. + - A single inner array produces a single line on the chart. + + In this example, 24 hourly CPU utilization samples are plotted as a single line. + Data points are evenly spaced along the X-axis (using the default Period of 1.0). +#> + +$ChartTitle = 'CPU Utilization - Last 24 Hours (%)' +$CpuData = [double[]]@(12, 18, 15, 10, 8, 11, 25, 42, 65, 71, 68, 72, 74, 70, 66, 63, 58, 55, 48, 40, 35, 28, 22, 16) +$Values = @(,$CpuData) + +<# + The New-SignalChart cmdlet generates the Signal Chart image. + + -Title : Sets the chart title displayed at the top of the image. + -Values : Array of double arrays. Each inner array is one signal line/series. + -LabelXAxis : Label for the X-axis. + -LabelYAxis : Label for the Y-axis. + -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-SignalChart ` + -Title $ChartTitle ` + -Values $Values ` + -LabelXAxis 'Hour' ` + -LabelYAxis 'CPU (%)' ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Filename 'Example07-SignalChart' diff --git a/Examples/Example08.ps1 b/Examples/Example08.ps1 new file mode 100644 index 0000000..b820be2 --- /dev/null +++ b/Examples/Example08.ps1 @@ -0,0 +1,83 @@ +<# + .SYNOPSIS + Example 08 - Signal Chart with DateTime X-Axis (Time-Series Data) + + .DESCRIPTION + This example demonstrates how to create a Signal Chart with a DateTime X-axis using the + AsBuiltReport.Chart module. + + When the -DateTimeTicksBottom switch is used, the X-axis is formatted as human-readable + date/time values. Data points are specified using OADate (OLE Automation Date) values which + can be obtained by calling .ToOADate() on a DateTime object. + + The chart displays hourly NFS read throughput for a NetApp ONTAP NAS volume over one day. +#> + +[CmdletBinding()] +param ( + [System.IO.FileInfo] $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 time range for the X-axis. + OADate (OLE Automation Date) values are used to represent DateTime as a floating-point number. + Each increment of 1.0/24 represents one hour. +#> + +$StartDate = (Get-Date '2024-06-01 00:00:00').ToOADate() +$HourCount = 24 + +# Build an array of OADate values spaced one hour apart. +$XValues = [double[]](0..($HourCount - 1) | ForEach-Object { $StartDate + ($_ / 24.0) }) + +<# + Hourly NFS read throughput in MB/s for a 24-hour window. + In a real-world scenario these values would come from your storage monitoring system. +#> + +$NfsRead = [double[]]@(10.2, 12.5, 8.7, 6.3, 5.1, 7.4, 11.8, 15.6, 18.2, 20.1, 22.4, 19.8, 17.3, 16.5, 18.9, 21.2, 23.6, 20.8, 17.4, 14.2, 12.1, 10.5, 9.3, 8.8) +$Values = @(,$NfsRead) + +<# + The New-SignalChart cmdlet generates the Signal Chart image. + + -Title : Sets the chart title displayed at the top of the image. + -Values : Array of double arrays. Each inner array is one signal line/series. + -ScatterXValues : Array of double arrays containing OADate X values for each series. + When provided, scatter mode is used (explicit X positions per point). + -DateTimeTicksBottom : Formats the X-axis as human-readable date/time labels. + -LabelXAxis : Label for the X-axis. + -LabelYAxis : Label for the Y-axis. + -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-SignalChart ` + -Title 'ONTAP NAS - NFS Read Throughput (MB/s)' ` + -Values $Values ` + -ScatterXValues @(,$XValues) ` + -DateTimeTicksBottom ` + -LabelXAxis 'Time' ` + -LabelYAxis 'Throughput (MB/s)' ` + -Width 700 ` + -Height 400 ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Filename 'Example08-SignalChart-DateTime' diff --git a/Examples/Example09.ps1 b/Examples/Example09.ps1 new file mode 100644 index 0000000..ede1fe8 --- /dev/null +++ b/Examples/Example09.ps1 @@ -0,0 +1,118 @@ +<# + .SYNOPSIS + Example 09 - Signal Chart with Multiple Lines + + .DESCRIPTION + This example demonstrates how to create a Signal Chart with multiple lines and advanced visual + options using the AsBuiltReport.Chart module. + + Features demonstrated: + - Multiple signal lines on a single chart + - Scatter mode with explicit X values and DateTime X-axis + - Legend with custom font size + - Custom color palette + - Bold title and axis labels + + The chart displays hourly NFS read and write throughput for a NetApp ONTAP NAS volume over + one day, simulating performance data that would typically come from a monitoring system. +#> + +[CmdletBinding()] +param ( + [System.IO.FileInfo] $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 time range for the X-axis. + OADate (OLE Automation Date) values are used to represent DateTime as a floating-point number. + Each increment of 1.0/24 represents one hour. +#> + +$StartDate = (Get-Date '2024-06-01 00:00:00').ToOADate() +$HourCount = 24 + +# Build an array of OADate values spaced one hour apart (shared by all lines). +$XValues = [double[]](0..($HourCount - 1) | ForEach-Object { $StartDate + ($_ / 24.0) }) + +<# + Hourly NFS throughput data in MB/s for a 24-hour window. + In a real-world scenario these values would come from your storage monitoring system. +#> + +$NfsRead = [double[]]@(10.2, 12.5, 8.7, 6.3, 5.1, 7.4, 11.8, 15.6, 18.2, 20.1, 22.4, 19.8, 17.3, 16.5, 18.9, 21.2, 23.6, 20.8, 17.4, 14.2, 12.1, 10.5, 9.3, 8.8) +$NfsWrite = [double[]]@(5.1, 6.2, 4.3, 3.1, 2.5, 3.7, 5.9, 7.8, 9.1, 10.0, 11.2, 9.9, 8.6, 8.2, 9.4, 10.6, 11.8, 10.4, 8.7, 7.1, 6.0, 5.2, 4.6, 4.4) + +# Each inner array is a separate line. The outer @() creates the list of series. +$Values = @($NfsRead, $NfsWrite) +$Labels = @('NFS Read', 'NFS Write') + +# ScatterXValues must match $Values in structure: one X array per Y array. +$ScatterXValues = @($XValues, $XValues) + +<# + Custom colors allow signal lines to be visually distinguished. + Colors are assigned in the same order as $Values. +#> + +$CustomColors = @('#3498DB', '#E74C3C') + +<# + The New-SignalChart cmdlet generates the Signal Chart image. + + -Title : Sets the chart title displayed at the top of the image. + -TitleFontSize : Sets the font size of the title in points. + -TitleFontBold : Renders the title in bold. + -Values : Array of double arrays. Each inner array is one signal line/series. + -Labels : Array of label strings, one per series (shown in the legend). + -ScatterXValues : Array of double arrays containing OADate X values for each series. + -DateTimeTicksBottom : Formats the X-axis as human-readable date/time labels. + -EnableLegend : Enables the legend on the chart. + -LegendOrientation : Sets legend layout direction (Vertical or Horizontal). + -LegendAlignment : Positions the legend (e.g. UpperRight, LowerCenter). + -LegendFontSize : Sets the legend text font size in points. + -LabelXAxis : Label for the X-axis. + -LabelYAxis : Label for the Y-axis. + -EnableCustomColorPalette : Enables use of the custom color palette. + -CustomColorPalette : Array of hex color strings, one per series. + -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-SignalChart ` + -Title 'ONTAP NAS - NFS Throughput (MB/s)' ` + -TitleFontSize 18 ` + -TitleFontBold ` + -Values $Values ` + -Labels $Labels ` + -ScatterXValues $ScatterXValues ` + -DateTimeTicksBottom ` + -EnableLegend ` + -LegendOrientation Vertical ` + -LegendAlignment UpperRight ` + -LegendFontSize 12 ` + -LabelXAxis 'Time' ` + -LabelYAxis 'Throughput (MB/s)' ` + -EnableCustomColorPalette ` + -CustomColorPalette $CustomColors ` + -Width 800 ` + -Height 450 ` + -Format $Format ` + -OutputFolderPath $OutputFolderPath ` + -Filename 'Example09-SignalChart-MultiLine' From db4fff92fee83b1e942209a6095080a8934be46f Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sun, 8 Mar 2026 23:30:59 -0400 Subject: [PATCH 29/62] Update CHANGELOG and enhance example scripts with additional parameters and fixes --- CHANGELOG.md | 5 +++++ Examples/Example01.ps1 | 2 ++ Examples/Example03.ps1 | 2 ++ Examples/Example04.ps1 | 2 +- Examples/Example05.ps1 | 14 +++++++++----- Examples/Example06.ps1 | 17 ++++++++++------- Examples/Example07.ps1 | 6 ++++-- Sources/StackedBarChart.cs | 16 ++++++++++++---- 8 files changed, 45 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0903e70..98a1372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Signal chart support - Add support for setting the background color of the chart +- Add documentation example on how to use the modules ### Changed Update module version to 0.3.0 +### Fixed + +- Fix issue with the stacked chart not rendering correctly when using certain data sets + ## [0.2.0] - 2026-02-20 ### Added diff --git a/Examples/Example01.ps1 b/Examples/Example01.ps1 index 44137fb..03a2aec 100644 --- a/Examples/Example01.ps1 +++ b/Examples/Example01.ps1 @@ -52,4 +52,6 @@ New-PieChart ` -Labels $Labels ` -Format $Format ` -OutputFolderPath $OutputFolderPath ` + -Width 600 ` + -Height 400 ` -Filename 'Example01-PieChart' diff --git a/Examples/Example03.ps1 b/Examples/Example03.ps1 index 437b87c..e2198b1 100644 --- a/Examples/Example03.ps1 +++ b/Examples/Example03.ps1 @@ -56,4 +56,6 @@ New-BarChart ` -LabelYAxis 'CPU (%)' ` -Format $Format ` -OutputFolderPath $OutputFolderPath ` + -Width 600 ` + -Height 600 ` -Filename 'Example03-BarChart' diff --git a/Examples/Example04.ps1 b/Examples/Example04.ps1 index c312e60..4baa56c 100644 --- a/Examples/Example04.ps1 +++ b/Examples/Example04.ps1 @@ -84,7 +84,7 @@ New-BarChart ` -LabelXAxis 'Memory (%)' ` -LabelYAxis 'Server' ` -AreaOrientation Horizontal ` - -AxesMarginsTop 1 ` + -AxesMarginsTop 0.15 ` -EnableChartBorder ` -ChartBorderColor Black ` -EnableCustomColorPalette ` diff --git a/Examples/Example05.ps1 b/Examples/Example05.ps1 index ccc4cea..16e06e9 100644 --- a/Examples/Example05.ps1 +++ b/Examples/Example05.ps1 @@ -42,14 +42,12 @@ $OutputFolderPath = Resolve-Path $Path - Bar 4 = datastore-04: 300 GB Used, 700 GB Free #> -$ChartTitle = 'Datastore Capacity (GB)' -$Labels = @('datastore-01', 'datastore-02', 'datastore-03', 'datastore-04') +$ChartTitle = 'Datastore Capacity (GB)' +$Labels = @('datastore-01', 'datastore-02', 'datastore-03', 'datastore-04') $LegendCategories = @('Used Space', 'Free Space') # Each inner array represents one category's values across all bars. -$UsedSpace = [double[]]@(800, 600, 1500, 300) -$FreeSpace = [double[]]@(200, 400, 500, 700) -$Values = @($UsedSpace, $FreeSpace) +$Values = @(@(800, 200), @(600, 400), @(1500, 500), @(300, 700)) <# The New-StackedBarChart cmdlet generates the Stacked Bar Chart image. @@ -63,6 +61,9 @@ $Values = @($UsedSpace, $FreeSpace) -LabelYAxis : Label for the Y-axis. -Format : Output file format (e.g. png, jpg, svg). -OutputFolderPath : Directory where the generated chart file will be saved. + -Width : Width of the output image in pixels. + -Height : Height of the output image in pixels. + -AreaOrientation : Orientation of the bars (Horizontal or Vertical). -Filename : Name of the output file (without extension). #> @@ -76,4 +77,7 @@ New-StackedBarChart ` -LabelYAxis 'Capacity (GB)' ` -Format $Format ` -OutputFolderPath $OutputFolderPath ` + -Width 700 ` + -Height 450 ` + -AreaOrientation Horizontal ` -Filename 'Example05-StackedBarChart' diff --git a/Examples/Example06.ps1 b/Examples/Example06.ps1 index 626d04d..d8d1cb0 100644 --- a/Examples/Example06.ps1 +++ b/Examples/Example06.ps1 @@ -44,15 +44,18 @@ $OutputFolderPath = Resolve-Path $Path In this example each bar represents a network adapter, and each stack segment is a traffic type. #> -$ChartTitle = 'Network Adapter Traffic (Mbps)' -$Labels = @('vmnic0', 'vmnic1', 'vmnic2', 'vmnic3') +$ChartTitle = 'Network Adapter Traffic (Mbps)' +$Labels = @('vmnic0', 'vmnic1', 'vmnic2', 'vmnic3') $LegendCategories = @('Inbound', 'Outbound', 'Dropped') -# Each inner array represents one traffic category across all adapters. -$Inbound = [double[]]@(450, 320, 280, 510) -$Outbound = [double[]]@(380, 290, 210, 460) -$Dropped = [double[]]@(15, 5, 30, 8) -$Values = @($Inbound, $Outbound, $Dropped) +# Values are organized by category (stack segment), so each inner array corresponds to one category across all bars. +# Example values for Inbound, Outbound, and Dropped traffic for each network adapter: +# - vmnic0: 450 Mbps Inbound, 320 Mbps Outbound, 280 Mbps Dropped +$vmnic0 = [double[]]@(450, 320, 280) +$vmnic1 = [double[]]@(380, 290, 210) +$vmnic2 = [double[]]@(150, 50, 300) +$vmnic3 = [double[]]@(200, 320, 300) +$Values = @($vmnic0, $vmnic1, $vmnic2, $vmnic3) <# A custom hex color palette can be used to match corporate branding or improve readability. diff --git a/Examples/Example07.ps1 b/Examples/Example07.ps1 index 51da8cb..df685bf 100644 --- a/Examples/Example07.ps1 +++ b/Examples/Example07.ps1 @@ -41,8 +41,8 @@ $OutputFolderPath = Resolve-Path $Path #> $ChartTitle = 'CPU Utilization - Last 24 Hours (%)' -$CpuData = [double[]]@(12, 18, 15, 10, 8, 11, 25, 42, 65, 71, 68, 72, 74, 70, 66, 63, 58, 55, 48, 40, 35, 28, 22, 16) -$Values = @(,$CpuData) +$CpuData = [double[]]@(12, 18, 15, 10, 8, 11, 25, 42, 65, 71, 68, 72, 74, 70, 66, 63, 58, 55, 48, 40, 35, 28, 22, 16) +$Values = @(, $CpuData) <# The New-SignalChart cmdlet generates the Signal Chart image. @@ -63,4 +63,6 @@ New-SignalChart ` -LabelYAxis 'CPU (%)' ` -Format $Format ` -OutputFolderPath $OutputFolderPath ` + -Width 700 ` + -Height 450 ` -Filename 'Example07-SignalChart' diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index d58b6d9..91b2ff7 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -82,14 +82,22 @@ public object Chart(List values, string[] labels, string[] categoryNam // assign values and colors to each bar if (values.Count > 0 && values[0].Length > 1) { - if (values.Count != categoryNames.Length) + // Validate that values.Length matches labels.Length + // This validation is necessary to ensure that each set of values corresponds to a label, which is crucial for accurate representation in the stacked bar chart. If the lengths do not match, it indicates a mismatch in the data structure, leading to potential errors in plotting and misinterpretation of the chart. + if (values.Count != labels.Length) { - throw new ArgumentException("Error: Values and category names must be equal."); + throw new ArgumentException("Error: Value sets and Label length must be equal."); } - if (values.Count != labels.Length) + // Validate that each set of values has the same length as category names + // This validation ensures that each category in the stacked bar chart has a corresponding value for each label. If the lengths do not match, it indicates an inconsistency in the data structure, which can lead to errors in plotting and misrepresentation of the chart. Each set of values must align with the category names to accurately reflect the data in the stacked bar chart. + foreach (var valueSet in values) { - throw new ArgumentException("Error: Values and labels must be equal."); + if (valueSet.Length != categoryNames.Length) + { + throw new ArgumentException("Error: Each set of values must have the same length as category names."); + } } + for (int x = 0; x < values.Count; x++) { double nextBarBase = 0; From 772e741c1fdaed485a507572dbdce41863c2e37b Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sun, 8 Mar 2026 23:37:24 -0400 Subject: [PATCH 30/62] Update error messages in New-StackedBarChart tests for clarity --- CHANGELOG.md | 1 + Tests/AsBuiltReport.Chart.Functions.Tests.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a1372..3ca6103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Update module version to 0.3.0 ### Fixed - Fix issue with the stacked chart not rendering correctly when using certain data sets +- Fix pester test to properly validate the functionality of the module ## [0.2.0] - 2026-02-20 diff --git a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 index 212d76e..b7ff429 100644 --- a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 +++ b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 @@ -68,10 +68,10 @@ Describe 'AsBuiltReport.Chart Exported Functions' { { New-StackedBarChart } | Should -Throw } It 'Should throw error for mismatched Values and Labels' { - { New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A') -LegendCategories @('X', 'Y') -OutputFolderPath $TestDrive -Format 'png' } | Should -Throw -ExpectedMessage "Error: Values and labels must be equal." + { New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A') -LegendCategories @('X', 'Y') -OutputFolderPath $TestDrive -Format 'png' } | Should -Throw -ExpectedMessage "Error: Value sets and Label length must be equal." } It 'Should throw error for mismatched Values and LegendCategories' { - { New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A', 'B') -LegendCategories @('X') -Format 'png' -OutputFolderPath $TestDrive } | Should -Throw -ExpectedMessage "Error: Values and category names must be equal." + { 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 From 864ea142deb9fbca279e1c92ead96d7c083a8feb Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sun, 8 Mar 2026 23:53:39 -0400 Subject: [PATCH 31/62] Add example index to README with descriptions for each chart example --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index aadec70..e4a1a28 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,22 @@ New-StackedBarChart -Title 'Test' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -L ``` ![StackedBarChart](./Samples/StackedBarChart.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 | + ## :x: Known Issues - No known issues at this time. From a10d5eb986cf5a4d5618be0c39a1e4b988e25557 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 9 Mar 2026 09:33:53 -0400 Subject: [PATCH 32/62] Enhance Example01 with additional parameters for chart customization --- Examples/Example01.ps1 | 4 ++++ Todo.md | 26 ++++++-------------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/Examples/Example01.ps1 b/Examples/Example01.ps1 index 03a2aec..fedb72a 100644 --- a/Examples/Example01.ps1 +++ b/Examples/Example01.ps1 @@ -43,6 +43,9 @@ $Labels = @('Powered On', 'Powered Off', 'Suspended') -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). #> @@ -54,4 +57,5 @@ New-PieChart ` -OutputFolderPath $OutputFolderPath ` -Width 600 ` -Height 400 ` + -ColorPalette Category20 ` -Filename 'Example01-PieChart' diff --git a/Todo.md b/Todo.md index ad6f254..e205db2 100644 --- a/Todo.md +++ b/Todo.md @@ -1,20 +1,6 @@ -- [x] Add option to set background color of the chart area - - [x] https://scottplot.net/cookbook/5/Styling/BackgroundColors/ - -```ScottPlot.Plot myPlot = new(); -// setup a plot with sample data -myPlot.Add.Signal(Generate.Sin(51)); -myPlot.Add.Signal(Generate.Cos(51)); -myPlot.XLabel("Horizontal Axis"); -myPlot.YLabel("Vertical Axis"); - -// some items must be styled directly -myPlot.FigureBackground.Color = Colors.Navy; -myPlot.DataBackground.Color = Colors.Navy.Darken(0.1); -myPlot.Grid.MajorLineColor = Colors.Navy.Lighten(0.1); - -// some items have helper methods to configure multiple properties at once -myPlot.Axes.Color(Colors.Navy.Lighten(0.8)); - -myPlot.SavePng("demo.png", 400, 300); -``` +- [] Add watermark support to charts + - [] Watermark should be configurable with parameters for text, font, size, color, and opacity + - [] Watermark should be applied to all chart types (bar, line, pie, etc.) + - [] Ensure watermark does not interfere with chart readability + - [] Update documentation with instructions on how to use watermark feature + - [] Add unit tests to verify watermark functionality across different chart types and configurations From 7045f97faa1424b6292434e3be54b083153c488a Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 9 Mar 2026 09:39:37 -0400 Subject: [PATCH 33/62] Fix typo in ColorPalette parameter description --- Examples/Example01.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example01.ps1 b/Examples/Example01.ps1 index fedb72a..accd97e 100644 --- a/Examples/Example01.ps1 +++ b/Examples/Example01.ps1 @@ -45,7 +45,7 @@ $Labels = @('Powered On', 'Powered Off', 'Suspended') -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 + -ColorPalette : Predefined color palette (e.g. Category20, Pastel). -Filename : Name of the output file (without extension). #> From 97816096af97584e3d0152b7c82b025c0ac4ce24 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 9 Mar 2026 10:48:41 -0400 Subject: [PATCH 34/62] Refactor validation logic in StackedBarChart to ensure consistency between values, labels, and category names --- Sources/StackedBarChart.cs | 70 +++++++++++--------------------------- 1 file changed, 19 insertions(+), 51 deletions(-) diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 91b2ff7..7cbfcfc 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -80,71 +80,39 @@ public object Chart(List values, string[] labels, string[] categoryNam // create bars var bars = new List(); // assign values and colors to each bar - if (values.Count > 0 && values[0].Length > 1) + // Validate that values.Length matches labels.Length + // This validation is necessary to ensure that each set of values corresponds to a label, which is crucial for accurate representation in the stacked bar chart. If the lengths do not match, it indicates a mismatch in the data structure, leading to potential errors in plotting and misinterpretation of the chart. + if (values.Count != labels.Length) { - // Validate that values.Length matches labels.Length - // This validation is necessary to ensure that each set of values corresponds to a label, which is crucial for accurate representation in the stacked bar chart. If the lengths do not match, it indicates a mismatch in the data structure, leading to potential errors in plotting and misinterpretation of the chart. - if (values.Count != labels.Length) - { - throw new ArgumentException("Error: Value sets and Label length must be equal."); - } - // Validate that each set of values has the same length as category names - // This validation ensures that each category in the stacked bar chart has a corresponding value for each label. If the lengths do not match, it indicates an inconsistency in the data structure, which can lead to errors in plotting and misrepresentation of the chart. Each set of values must align with the category names to accurately reflect the data in the stacked bar chart. - foreach (var valueSet in values) - { - if (valueSet.Length != categoryNames.Length) - { - throw new ArgumentException("Error: Each set of values must have the same length as category names."); - } - } - - for (int x = 0; x < values.Count; x++) - { - double nextBarBase = 0; - for (int i = 0; i < values[x].Length; i++) - { - if (colorPalette != null) - { - bars.Add(new ScottPlot.Bar - { - Position = x, - Value = nextBarBase + values[x][i], - ValueBase = nextBarBase, - FillColor = colorPalette.GetColor(i), - Label = $"{values[x][i]}", - CenterLabel = true, - }); - nextBarBase += values[x][i]; - } - } - } + throw new ArgumentException("Error: Value sets and Label length must be equal."); } - else + // Validate that each set of values has the same length as category names + // This validation ensures that each category in the stacked bar chart has a corresponding value for each label. If the lengths do not match, it indicates an inconsistency in the data structure, which can lead to errors in plotting and misrepresentation of the chart. Each set of values must align with the category names to accurately reflect the data in the stacked bar chart. + foreach (var valueSet in values) { - if (values.Count != categoryNames.Length) + if (valueSet.Length != categoryNames.Length) { - throw new ArgumentException("Error: Values and category names must be equal."); - } - if (values[0].Length != labels.Length) - { - throw new ArgumentException("Error: Values and labels must be equal."); + throw new ArgumentException("Error: Each set of values must have the same length as category names."); } + } + for (int x = 0; x < values.Count; x++) + { double nextBarBase = 0; - for (int x = 0; x < values.Count; x++) + for (int i = 0; i < values[x].Length; i++) { if (colorPalette != null) { bars.Add(new ScottPlot.Bar { - Position = 0, - Value = nextBarBase + values[x][0], + Position = x, + Value = nextBarBase + values[x][i], ValueBase = nextBarBase, - FillColor = colorPalette.GetColor(x), - Label = $"{values[x][0]}", + FillColor = colorPalette.GetColor(i), + Label = $"{values[x][i]}", CenterLabel = true, }); - nextBarBase += values[x][0]; + nextBarBase += values[x][i]; } } } @@ -269,7 +237,7 @@ public object Chart(List values, string[] labels, string[] categoryNam } // Set axis limits if values are empty or contain only one value to prevent auto-scaling issues - if (!(values.Count > 0 && values[0].Length > 1)) + if (values.Count == 1) { if (AreaOrientation == Orientations.Horizontal) { From e9d9b3cb41884c40fbb3afb597e88bd27a93d52c Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 9 Mar 2026 11:03:09 -0400 Subject: [PATCH 35/62] Reformat Todo list for watermark feature to improve readability --- Todo.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Todo.md b/Todo.md index e205db2..88dd57d 100644 --- a/Todo.md +++ b/Todo.md @@ -1,6 +1,6 @@ -- [] Add watermark support to charts - - [] Watermark should be configurable with parameters for text, font, size, color, and opacity - - [] Watermark should be applied to all chart types (bar, line, pie, etc.) - - [] Ensure watermark does not interfere with chart readability - - [] Update documentation with instructions on how to use watermark feature - - [] Add unit tests to verify watermark functionality across different chart types and configurations +- [ ] Add watermark support to charts + - [ ] Watermark should be configurable with parameters for text, font, size, color, and opacity + - [ ] Watermark should be applied to all chart types (bar, line, pie, etc.) + - [ ] Ensure watermark does not interfere with chart readability + - [ ] Update documentation with instructions on how to use watermark feature + - [ ] Add unit tests to verify watermark functionality across different chart types and configurations From fa2478afa4be26fe828577ab56b35729ef852135 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:14:57 +0000 Subject: [PATCH 36/62] Initial plan From abb246b9b280e4ad0e33764b4f8d22959aa96a8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:27:35 +0000 Subject: [PATCH 37/62] Add watermark support to all chart types Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> --- README.md | 34 +++++++++ Sources/BarChart.cs | 3 + Sources/Chart.cs | 38 ++++++++++ Sources/PieChart.cs | 3 + Sources/PowerShell/BarChartPwsh.cs | 27 +++++++ Sources/PowerShell/PieChartPwsh.cs | 27 +++++++ Sources/PowerShell/SignalChartPwsh.cs | 27 +++++++ Sources/PowerShell/StackedBarChartPwsh.cs | 27 +++++++ Sources/SignalChart.cs | 3 + Sources/StackedBarChart.cs | 3 + Tests/AsBuiltReport.Chart.Functions.Tests.ps1 | 76 +++++++++++++++++++ Todo.md | 12 +-- 12 files changed, 274 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e4a1a28..fd24905 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,40 @@ All examples in the latest release of AsBuiltReport.Chart can be found in the ta | [Example8](./Examples/Example08.ps1) | Signal Chart with DateTime X-Axis (Time-Series Data) | | [Example9](./Examples/Example09.ps1) | Signal Chart with Multiple Lines | +## :watermark: Watermark Support + +All chart types support an optional watermark that overlays semi-transparent text in the center of the chart. The watermark is **disabled by default** and is activated only when the `-EnableWatermark` switch is supplied. + +### Watermark Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `EnableWatermark` | Switch | (off) | Enables the watermark overlay. | +| `WatermarkText` | String | `AsBuiltReport` | 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 + +```powershell +# Pie chart with default watermark (gray "AsBuiltReport" at 30% opacity) +New-PieChart -Title 'Sales' -Values @(10, 20, 30) -Labels @('A', 'B', 'C') -Format 'png' -EnableWatermark + +# Bar chart with a custom watermark text, color, and opacity +New-BarChart -Title 'Revenue' -Values @(100, 200, 150) -Labels @('Q1', 'Q2', 'Q3') -Format 'png' ` + -EnableWatermark -WatermarkText 'CONFIDENTIAL' -WatermarkColor Red -WatermarkOpacity 0.2 + +# Stacked bar chart with a larger watermark font +New-StackedBarChart -Title 'Budget' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -LegendCategories @('X','Y') -Format 'png' ` + -EnableWatermark -WatermarkFontSize 36 -WatermarkText 'DRAFT' + +# Signal chart with a custom font and opacity +New-SignalChart -Title 'Throughput' -Values @(,[double[]]@(1,2,3,4,5)) -Format 'png' ` + -EnableWatermark -WatermarkFontName 'Arial' -WatermarkOpacity 0.5 +``` + ## :x: Known Issues - No known issues at this time. diff --git a/Sources/BarChart.cs b/Sources/BarChart.cs index fec6bf8..f95e567 100644 --- a/Sources/BarChart.cs +++ b/Sources/BarChart.cs @@ -210,6 +210,9 @@ public object Chart(double[] values, string[] labels, string filename = "output" } } + // Apply watermark if enabled + ApplyWatermark(myPlot); + // Set filepath string Filepath = _outputFolderPath ?? System.IO.Directory.GetCurrentDirectory(); diff --git a/Sources/Chart.cs b/Sources/Chart.cs index 2d7c4b4..3c682cb 100644 --- a/Sources/Chart.cs +++ b/Sources/Chart.cs @@ -324,6 +324,44 @@ public static double AxesMarginsRight public static BasicColors? FigureBackgroundColor { get; set; } public static BasicColors? DataBackgroundColor { get; set; } + // Watermark settings (All Charts) + public static bool EnableWatermark { get; set; } + public static string WatermarkText { get; set; } = "AsBuiltReport"; + public static string WatermarkFontName { get; set; } = "Arial"; + public static int WatermarkFontSize { get; set; } = 24; + public static BasicColors WatermarkColor { get; set; } = BasicColors.Gray; + + internal static double _watermarkOpacity = 0.3; + public static double WatermarkOpacity + { + get { return _watermarkOpacity; } + set + { + if (value >= 0.0 && value <= 1.0) + { + _watermarkOpacity = value; + } + else + { + throw new ArgumentException("Error: WatermarkOpacity value range must be from 0.0 to 1.0."); + } + } + } + + internal static void ApplyWatermark(Plot plot) + { + if (!EnableWatermark || string.IsNullOrEmpty(WatermarkText)) + return; + + var annotation = plot.Add.Annotation(WatermarkText, Alignment.MiddleCenter); + annotation.LabelFontColor = ColorMap[WatermarkColor].WithOpacity(WatermarkOpacity); + annotation.LabelFontSize = WatermarkFontSize; + annotation.LabelFontName = WatermarkFontName; + annotation.LabelBackgroundColor = Colors.Transparent; + annotation.LabelBorderColor = Colors.Transparent; + annotation.LabelBorderWidth = 0; + } + public static Color GetDrawingColor(BasicColors color) { return ColorMap[color]; diff --git a/Sources/PieChart.cs b/Sources/PieChart.cs index 64d135d..4b610c3 100644 --- a/Sources/PieChart.cs +++ b/Sources/PieChart.cs @@ -108,6 +108,9 @@ public object Chart(double[] values, string[] labels, string filename = "output" myPlot.DataBackground.Color = GetDrawingColor(DataBackgroundColor.Value); } + // Apply watermark if enabled + ApplyWatermark(myPlot); + // Set filetpath to save string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); diff --git a/Sources/PowerShell/BarChartPwsh.cs b/Sources/PowerShell/BarChartPwsh.cs index 0c3105b..4362db3 100644 --- a/Sources/PowerShell/BarChartPwsh.cs +++ b/Sources/PowerShell/BarChartPwsh.cs @@ -127,6 +127,25 @@ public class NewBarChartCommand : Cmdlet [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 'AsBuiltReport'.")] + public string WatermarkText { get; set; } = "AsBuiltReport"; + + [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; + // Set chart Size WxH [Parameter(Mandatory = false, HelpMessage = "Width of the output chart in pixels. Defaults to 400.")] public int Width { get; set; } = 400; @@ -220,6 +239,14 @@ protected override void ProcessRecord() Chart.FigureBackgroundColor = FigureBackgroundColor; Chart.DataBackgroundColor = DataBackgroundColor; + // Watermark settings + Chart.EnableWatermark = EnableWatermark; + Chart.WatermarkText = WatermarkText; + Chart.WatermarkFontName = WatermarkFontName; + Chart.WatermarkFontSize = WatermarkFontSize; + Chart.WatermarkColor = WatermarkColor; + Chart.WatermarkOpacity = WatermarkOpacity; + // Set file directory save path Chart.OutputFolderPath = OutputFolderPath; diff --git a/Sources/PowerShell/PieChartPwsh.cs b/Sources/PowerShell/PieChartPwsh.cs index 9b0abd6..68266e9 100644 --- a/Sources/PowerShell/PieChartPwsh.cs +++ b/Sources/PowerShell/PieChartPwsh.cs @@ -121,6 +121,25 @@ public class NewPieChartCommand : Cmdlet [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 'AsBuiltReport'.")] + public string WatermarkText { get; set; } = "AsBuiltReport"; + + [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; + // Set chart Size WxH [Parameter(Mandatory = false, HelpMessage = "Width of the chart in pixels. Defaults to 400.")] public int Width { get; set; } = 400; @@ -210,6 +229,14 @@ protected override void ProcessRecord() Chart.FigureBackgroundColor = FigureBackgroundColor; Chart.DataBackgroundColor = DataBackgroundColor; + // Watermark settings + Chart.EnableWatermark = EnableWatermark; + Chart.WatermarkText = WatermarkText; + Chart.WatermarkFontName = WatermarkFontName; + Chart.WatermarkFontSize = WatermarkFontSize; + Chart.WatermarkColor = WatermarkColor; + Chart.WatermarkOpacity = WatermarkOpacity; + // Set file directory save path Chart.OutputFolderPath = OutputFolderPath; diff --git a/Sources/PowerShell/SignalChartPwsh.cs b/Sources/PowerShell/SignalChartPwsh.cs index 0b7ddda..6202b2d 100644 --- a/Sources/PowerShell/SignalChartPwsh.cs +++ b/Sources/PowerShell/SignalChartPwsh.cs @@ -136,6 +136,25 @@ public class NewSignalChartCommand : Cmdlet [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 'AsBuiltReport'.")] + public string WatermarkText { get; set; } = "AsBuiltReport"; + + [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; + // Set chart Size WxH [Parameter(Mandatory = false, HelpMessage = "Width of the output chart in pixels. Defaults to 400.")] public int Width { get; set; } = 400; @@ -226,6 +245,14 @@ protected override void ProcessRecord() Chart.FigureBackgroundColor = FigureBackgroundColor; Chart.DataBackgroundColor = DataBackgroundColor; + // Watermark settings + Chart.EnableWatermark = EnableWatermark; + Chart.WatermarkText = WatermarkText; + Chart.WatermarkFontName = WatermarkFontName; + Chart.WatermarkFontSize = WatermarkFontSize; + Chart.WatermarkColor = WatermarkColor; + Chart.WatermarkOpacity = WatermarkOpacity; + // Set file directory save path Chart.OutputFolderPath = OutputFolderPath; diff --git a/Sources/PowerShell/StackedBarChartPwsh.cs b/Sources/PowerShell/StackedBarChartPwsh.cs index 09e728c..aa9b407 100644 --- a/Sources/PowerShell/StackedBarChartPwsh.cs +++ b/Sources/PowerShell/StackedBarChartPwsh.cs @@ -131,6 +131,25 @@ public class NewStackedBarChartCommand : Cmdlet [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 'AsBuiltReport'.")] + public string WatermarkText { get; set; } = "AsBuiltReport"; + + [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; + // Set chart Size WxH [Parameter(Mandatory = false, HelpMessage = "Set the width of the chart in pixels.")] public int Width { get; set; } = 400; @@ -223,6 +242,14 @@ protected override void ProcessRecord() Chart.FigureBackgroundColor = FigureBackgroundColor; Chart.DataBackgroundColor = DataBackgroundColor; + // Watermark settings + Chart.EnableWatermark = EnableWatermark; + Chart.WatermarkText = WatermarkText; + Chart.WatermarkFontName = WatermarkFontName; + Chart.WatermarkFontSize = WatermarkFontSize; + Chart.WatermarkColor = WatermarkColor; + Chart.WatermarkOpacity = WatermarkOpacity; + // Set file directory save path Chart.OutputFolderPath = OutputFolderPath; diff --git a/Sources/SignalChart.cs b/Sources/SignalChart.cs index 7d8fb1e..a59c048 100644 --- a/Sources/SignalChart.cs +++ b/Sources/SignalChart.cs @@ -171,6 +171,9 @@ public object Chart(List values, string[] labels, double xOffset = 0, myPlot.DataBackground.Color = GetDrawingColor(DataBackgroundColor.Value); } + // Apply watermark if enabled + ApplyWatermark(myPlot); + // Set filepath string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 7cbfcfc..2270be3 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -249,6 +249,9 @@ public object Chart(List values, string[] labels, string[] categoryNam } } + // Apply watermark if enabled + ApplyWatermark(myPlot); + // Set filepath string Filepath = _outputFolderPath ?? Directory.GetCurrentDirectory(); diff --git a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 index b7ff429..7c7d375 100644 --- a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 +++ b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 @@ -175,4 +175,80 @@ Describe 'AsBuiltReport.Chart Exported Functions' { } } } + + Context 'Watermark support' { + Context 'New-PieChart with watermark' { + It 'Should run without error with watermark enabled using defaults' { + { New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark } | Should -Not -Throw + } + It 'Should return a file when watermark is enabled' { + $result = New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -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 and color' { + { New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark -WatermarkText 'Confidential' -WatermarkColor Red } | Should -Not -Throw + } + It 'Should run without error with custom watermark font and size' { + { New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark -WatermarkFontSize 36 -WatermarkFontName 'Arial' } | Should -Not -Throw + } + It 'Should run without error with custom watermark opacity' { + { New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark -WatermarkOpacity 0.5 } | Should -Not -Throw + } + It 'Should run without error without watermark (disabled by default)' { + { New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + } + + Context 'New-BarChart with watermark' { + It 'Should run without error with watermark enabled using defaults' { + { New-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark } | Should -Not -Throw + } + It 'Should return a file when watermark is enabled' { + $result = New-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -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-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark -WatermarkColor Blue -WatermarkOpacity 0.2 } | Should -Not -Throw + } + It 'Should run without error without watermark (disabled by default)' { + { New-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + } + + Context 'New-StackedBarChart with watermark' { + It 'Should run without error with watermark enabled using defaults' { + { New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A', 'B') -LegendCategories @('X', 'Y') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark } | Should -Not -Throw + } + It 'Should return a file when watermark is enabled' { + $result = New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A', 'B') -LegendCategories @('X', 'Y') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark + $result | Should -BeOfType 'System.IO.FileSystemInfo' + Test-Path $result | Should -BeTrue + } + It 'Should run without error with custom watermark font and size' { + { New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A', 'B') -LegendCategories @('X', 'Y') -Format 'png' -OutputFolderPath $TestDrive -EnableWatermark -WatermarkFontSize 18 -WatermarkText 'Draft' } | Should -Not -Throw + } + It 'Should run without error without watermark (disabled by default)' { + { New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A', 'B') -LegendCategories @('X', 'Y') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + } + + 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 + } + 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 | 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 + } + 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 + } + } + } } diff --git a/Todo.md b/Todo.md index 88dd57d..edb52a8 100644 --- a/Todo.md +++ b/Todo.md @@ -1,6 +1,6 @@ -- [ ] Add watermark support to charts - - [ ] Watermark should be configurable with parameters for text, font, size, color, and opacity - - [ ] Watermark should be applied to all chart types (bar, line, pie, etc.) - - [ ] Ensure watermark does not interfere with chart readability - - [ ] Update documentation with instructions on how to use watermark feature - - [ ] Add unit tests to verify watermark functionality across different chart types and configurations +- [x] Add watermark support to charts + - [x] Watermark should be configurable with parameters for text, font, size, color, and opacity + - [x] Watermark should be applied to all chart types (bar, line, pie, etc.) + - [x] Ensure watermark does not interfere with chart readability + - [x] Update documentation with instructions on how to use watermark feature + - [x] Add unit tests to verify watermark functionality across different chart types and configurations From b6482a6db367cd55fa7b218aa8152d65118b23ee Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 9 Mar 2026 17:14:22 -0400 Subject: [PATCH 38/62] Add watermark support with customizable text, alignment, and rotation for charts --- Sources/Chart.cs | 24 +++++++++++++++++++++-- Sources/PowerShell/BarChartPwsh.cs | 12 ++++++++++-- Sources/PowerShell/PieChartPwsh.cs | 12 ++++++++++-- Sources/PowerShell/SignalChartPwsh.cs | 12 ++++++++++-- Sources/PowerShell/StackedBarChartPwsh.cs | 12 ++++++++++-- 5 files changed, 62 insertions(+), 10 deletions(-) diff --git a/Sources/Chart.cs b/Sources/Chart.cs index 3c682cb..f0a18bc 100644 --- a/Sources/Chart.cs +++ b/Sources/Chart.cs @@ -254,6 +254,19 @@ public static bool IsValidHexColor(string hexCode) { BasicColors.DarkGreen, Colors.DarkGreen }, }; + internal static readonly IReadOnlyDictionary WatermarkAlignmentMap = new Dictionary() + { + {Alignments.LowerCenter, Alignment.LowerCenter}, + {Alignments.LowerLeft,Alignment.LowerLeft}, + {Alignments.LowerRight, Alignment.LowerRight}, + {Alignments.MiddleCenter,Alignment.MiddleCenter}, + {Alignments.MiddleLeft, Alignment.MiddleLeft}, + {Alignments.MiddleRight,Alignment.MiddleRight}, + {Alignments.UpperCenter,Alignment.UpperCenter}, + {Alignments.UpperLeft,Alignment.UpperLeft}, + {Alignments.UpperRight, Alignment.UpperRight}, + }; + // Set area axes margins internal static double _axesMarginsTop = 0.07; public static double AxesMarginsTop @@ -326,7 +339,12 @@ public static double AxesMarginsRight // Watermark settings (All Charts) public static bool EnableWatermark { get; set; } - public static string WatermarkText { get; set; } = "AsBuiltReport"; + public static string WatermarkText { get; set; } = "Confidential"; + + public static Alignments WatermarkAlignment { get; set; } = Alignments.MiddleCenter; + + public static float WatermarkRotation { get; set; } = 0; + public static string WatermarkFontName { get; set; } = "Arial"; public static int WatermarkFontSize { get; set; } = 24; public static BasicColors WatermarkColor { get; set; } = BasicColors.Gray; @@ -353,13 +371,15 @@ internal static void ApplyWatermark(Plot plot) if (!EnableWatermark || string.IsNullOrEmpty(WatermarkText)) return; - var annotation = plot.Add.Annotation(WatermarkText, Alignment.MiddleCenter); + var annotation = plot.Add.Annotation(WatermarkText, WatermarkAlignmentMap[WatermarkAlignment]); annotation.LabelFontColor = ColorMap[WatermarkColor].WithOpacity(WatermarkOpacity); annotation.LabelFontSize = WatermarkFontSize; annotation.LabelFontName = WatermarkFontName; annotation.LabelBackgroundColor = Colors.Transparent; annotation.LabelBorderColor = Colors.Transparent; annotation.LabelBorderWidth = 0; + annotation.LabelShadowColor = Colors.Transparent; + annotation.LabelRotation = WatermarkRotation; } public static Color GetDrawingColor(BasicColors color) diff --git a/Sources/PowerShell/BarChartPwsh.cs b/Sources/PowerShell/BarChartPwsh.cs index 4362db3..c805346 100644 --- a/Sources/PowerShell/BarChartPwsh.cs +++ b/Sources/PowerShell/BarChartPwsh.cs @@ -131,8 +131,11 @@ public class NewBarChartCommand : Cmdlet [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 'AsBuiltReport'.")] - public string WatermarkText { get; set; } = "AsBuiltReport"; + [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"; @@ -146,6 +149,9 @@ public class NewBarChartCommand : Cmdlet [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 output chart in pixels. Defaults to 400.")] public int Width { get; set; } = 400; @@ -241,11 +247,13 @@ protected override void ProcessRecord() // Watermark settings Chart.EnableWatermark = EnableWatermark; + Chart.WatermarkAlignment = WatermarkAlignment; Chart.WatermarkText = WatermarkText; Chart.WatermarkFontName = WatermarkFontName; Chart.WatermarkFontSize = WatermarkFontSize; Chart.WatermarkColor = WatermarkColor; Chart.WatermarkOpacity = WatermarkOpacity; + Chart.WatermarkRotation = WatermarkRotation; // Set file directory save path Chart.OutputFolderPath = OutputFolderPath; diff --git a/Sources/PowerShell/PieChartPwsh.cs b/Sources/PowerShell/PieChartPwsh.cs index 68266e9..8604c3f 100644 --- a/Sources/PowerShell/PieChartPwsh.cs +++ b/Sources/PowerShell/PieChartPwsh.cs @@ -125,8 +125,11 @@ public class NewPieChartCommand : Cmdlet [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 'AsBuiltReport'.")] - public string WatermarkText { get; set; } = "AsBuiltReport"; + [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"; @@ -140,6 +143,9 @@ public class NewPieChartCommand : Cmdlet [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; @@ -232,10 +238,12 @@ protected override void ProcessRecord() // 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; diff --git a/Sources/PowerShell/SignalChartPwsh.cs b/Sources/PowerShell/SignalChartPwsh.cs index 6202b2d..dbb3752 100644 --- a/Sources/PowerShell/SignalChartPwsh.cs +++ b/Sources/PowerShell/SignalChartPwsh.cs @@ -140,8 +140,11 @@ public class NewSignalChartCommand : Cmdlet [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 'AsBuiltReport'.")] - public string WatermarkText { get; set; } = "AsBuiltReport"; + [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"; @@ -155,6 +158,9 @@ public class NewSignalChartCommand : Cmdlet [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 output chart in pixels. Defaults to 400.")] public int Width { get; set; } = 400; @@ -248,10 +254,12 @@ protected override void ProcessRecord() // 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; diff --git a/Sources/PowerShell/StackedBarChartPwsh.cs b/Sources/PowerShell/StackedBarChartPwsh.cs index aa9b407..2875ce5 100644 --- a/Sources/PowerShell/StackedBarChartPwsh.cs +++ b/Sources/PowerShell/StackedBarChartPwsh.cs @@ -135,8 +135,11 @@ public class NewStackedBarChartCommand : Cmdlet [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 'AsBuiltReport'.")] - public string WatermarkText { get; set; } = "AsBuiltReport"; + [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"; @@ -150,6 +153,9 @@ public class NewStackedBarChartCommand : Cmdlet [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 = "Set the width of the chart in pixels.")] public int Width { get; set; } = 400; @@ -245,10 +251,12 @@ protected override void ProcessRecord() // 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; From 891b8055d831d52193ff5d1d69db1fe2bac1d954 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 9 Mar 2026 17:23:19 -0400 Subject: [PATCH 39/62] Update Todo.md --- Todo.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Todo.md b/Todo.md index edb52a8..8b13789 100644 --- a/Todo.md +++ b/Todo.md @@ -1,6 +1 @@ -- [x] Add watermark support to charts - - [x] Watermark should be configurable with parameters for text, font, size, color, and opacity - - [x] Watermark should be applied to all chart types (bar, line, pie, etc.) - - [x] Ensure watermark does not interfere with chart readability - - [x] Update documentation with instructions on how to use watermark feature - - [x] Add unit tests to verify watermark functionality across different chart types and configurations + From ae53b3fdffa4d7ec373cac4fe706f5ff88285a7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:02:51 +0000 Subject: [PATCH 40/62] Initial plan From 4da64bd0126dc2b4d8bacb0b8932b46f3aac2313 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:10:47 +0000 Subject: [PATCH 41/62] Fix jpg/jpeg extension swap, add Chart.Reset() for state isolation, improve exception types, fix typo Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> --- Sources/BarChart.cs | 2 +- Sources/Chart.cs | 63 +++++++++++++++++-- Sources/PieChart.cs | 2 +- Sources/PowerShell/BarChartPwsh.cs | 3 +- Sources/PowerShell/PieChartPwsh.cs | 3 +- Sources/PowerShell/SignalChartPwsh.cs | 3 +- Sources/PowerShell/StackedBarChartPwsh.cs | 5 +- Sources/SignalChart.cs | 2 +- Sources/StackedBarChart.cs | 2 +- Tests/AsBuiltReport.Chart.Functions.Tests.ps1 | 44 +++++++++++++ 10 files changed, 114 insertions(+), 15 deletions(-) diff --git a/Sources/BarChart.cs b/Sources/BarChart.cs index f95e567..47cd022 100644 --- a/Sources/BarChart.cs +++ b/Sources/BarChart.cs @@ -22,7 +22,7 @@ public object Chart(double[] values, string[] labels, string filename = "output" } else { - throw new Exception("CustomColorPalette is empty. Please provide valid color values."); + throw new InvalidOperationException("CustomColorPalette is empty. Please provide valid color values."); } } else diff --git a/Sources/Chart.cs b/Sources/Chart.cs index f0a18bc..39535db 100644 --- a/Sources/Chart.cs +++ b/Sources/Chart.cs @@ -386,6 +386,57 @@ public static Color GetDrawingColor(BasicColors color) { return ColorMap[color]; } + + internal static void Reset() + { + Format = Formats.png; + Title = null; + TitleFontBold = false; + TitleFontSize = 14; + TitleFontColor = BasicColors.Black; + FontName = "Arial"; + LabelFontSize = 14; + LabelFontColor = BasicColors.Black; + LabelBold = false; + LabelYAxis = "Count"; + LabelXAxis = "Values"; + _labelDistance = 0.6; + AreaOrientation = Orientations.Vertical; + _areaExplodeFraction = 0; + EnableLegend = false; + LegendFontSize = 12; + LegendFontColor = BasicColors.Black; + LegendBold = false; + LegendBorderStyle = BorderStyles.Solid; + LegendBorderSize = 1; + LegendBorderColor = BasicColors.Black; + LegendOrientation = Orientations.Vertical; + LegendAlignment = Alignments.LowerRight; + EnableChartBorder = false; + ChartBorderStyle = BorderStyles.Solid; + ChartBorderSize = 1; + ChartBorderColor = BasicColors.Black; + colorPalette = ColorPaletteMap[ColorPalettes.Category10]; + InvertCustomColorPalette = false; + _customColorPalette = null; + EnableCustomColorPalette = false; + _outputFolderPath = null; + _axesMarginsTop = 0.07; + _axesMarginsDown = 0.07; + _axesMarginsLeft = 0.05; + _axesMarginsRight = 0.05; + FigureBackgroundColor = null; + DataBackgroundColor = null; + EnableWatermark = false; + WatermarkText = "Confidential"; + WatermarkAlignment = Alignments.MiddleCenter; + WatermarkRotation = 0; + WatermarkFontName = "Arial"; + WatermarkFontSize = 24; + WatermarkColor = BasicColors.Gray; + _watermarkOpacity = 0.3; + } + public static string GenerateToken(Byte length) { var bytes = new byte[length]; @@ -411,10 +462,10 @@ public static object SaveInFormat(Plot plot, int width, int height, string filep throw new ArgumentException("Error: Unable to Export Chart Exception"); } case Formats.jpg: - plot.SaveJpeg(Path.Combine(filepath, $"{filename}.jpeg"), width, height); - if (File.Exists(Path.Combine(filepath, $"{filename}.jpeg"))) + plot.SaveJpeg(Path.Combine(filepath, $"{filename}.jpg"), width, height); + if (File.Exists(Path.Combine(filepath, $"{filename}.jpg"))) { - FileInfo fileInfo = new FileInfo(Path.Combine(filepath, $"{filename}.jpeg")); + FileInfo fileInfo = new FileInfo(Path.Combine(filepath, $"{filename}.jpg")); return fileInfo; } else @@ -422,10 +473,10 @@ public static object SaveInFormat(Plot plot, int width, int height, string filep throw new ArgumentException("Error: Unable to Export Chart Exception"); } case Formats.jpeg: - plot.SaveJpeg(Path.Combine(filepath, $"{filename}.jpg"), width, height); - if (File.Exists(Path.Combine(filepath, $"{filename}.jpg"))) + plot.SaveJpeg(Path.Combine(filepath, $"{filename}.jpeg"), width, height); + if (File.Exists(Path.Combine(filepath, $"{filename}.jpeg"))) { - FileInfo fileInfo = new FileInfo(Path.Combine(filepath, $"{filename}.jpg")); + FileInfo fileInfo = new FileInfo(Path.Combine(filepath, $"{filename}.jpeg")); return fileInfo; } else diff --git a/Sources/PieChart.cs b/Sources/PieChart.cs index 4b610c3..12127bb 100644 --- a/Sources/PieChart.cs +++ b/Sources/PieChart.cs @@ -21,7 +21,7 @@ public object Chart(double[] values, string[] labels, string filename = "output" } else { - throw new Exception("CustomColorPalette is empty. Please provide valid color values."); + throw new InvalidOperationException("CustomColorPalette is empty. Please provide valid color values."); } } diff --git a/Sources/PowerShell/BarChartPwsh.cs b/Sources/PowerShell/BarChartPwsh.cs index c805346..262f0a0 100644 --- a/Sources/PowerShell/BarChartPwsh.cs +++ b/Sources/PowerShell/BarChartPwsh.cs @@ -165,6 +165,7 @@ public class NewBarChartCommand : Cmdlet protected override void ProcessRecord() { + Chart.Reset(); if (Values != null && Labels != null) { @@ -205,7 +206,7 @@ protected override void ProcessRecord() } else { - throw new Exception("EnableCustomColorPalette requires CustomColorPalette to be set."); + throw new InvalidOperationException("EnableCustomColorPalette requires CustomColorPalette to be set."); } } else diff --git a/Sources/PowerShell/PieChartPwsh.cs b/Sources/PowerShell/PieChartPwsh.cs index 8604c3f..5ccf2c2 100644 --- a/Sources/PowerShell/PieChartPwsh.cs +++ b/Sources/PowerShell/PieChartPwsh.cs @@ -160,6 +160,7 @@ public class NewPieChartCommand : Cmdlet protected override void ProcessRecord() { + Chart.Reset(); if (Values != null && Labels != null) { if (EnableLegend) @@ -199,7 +200,7 @@ protected override void ProcessRecord() } else { - throw new Exception("EnableCustomColorPalette requires CustomColorPalette to be set."); + throw new InvalidOperationException("EnableCustomColorPalette requires CustomColorPalette to be set."); } } else diff --git a/Sources/PowerShell/SignalChartPwsh.cs b/Sources/PowerShell/SignalChartPwsh.cs index dbb3752..555c452 100644 --- a/Sources/PowerShell/SignalChartPwsh.cs +++ b/Sources/PowerShell/SignalChartPwsh.cs @@ -174,6 +174,7 @@ public class NewSignalChartCommand : Cmdlet protected override void ProcessRecord() { + Chart.Reset(); if (Values != null) { if (EnableLegend) @@ -214,7 +215,7 @@ protected override void ProcessRecord() } else { - throw new Exception("EnableCustomColorPalette requires CustomColorPalette to be set."); + throw new InvalidOperationException("EnableCustomColorPalette requires CustomColorPalette to be set."); } } else diff --git a/Sources/PowerShell/StackedBarChartPwsh.cs b/Sources/PowerShell/StackedBarChartPwsh.cs index 2875ce5..94c5ca3 100644 --- a/Sources/PowerShell/StackedBarChartPwsh.cs +++ b/Sources/PowerShell/StackedBarChartPwsh.cs @@ -21,7 +21,7 @@ public class NewStackedBarChartCommand : Cmdlet [Parameter(Mandatory = true, HelpMessage = "Provide an array of strings for the labels of each bar in the chart.")] public string[] Labels { get; set; } - [Parameter(Mandatory = true, HelpMessage = "Povide an array of strings for the legend categories in the chart.")] + [Parameter(Mandatory = true, HelpMessage = "Provide an array of strings for the legend categories in the chart.")] public string[] LegendCategories { get; set; } // Title settings @@ -169,6 +169,7 @@ public class NewStackedBarChartCommand : Cmdlet protected override void ProcessRecord() { + Chart.Reset(); if (Values != null && Labels != null && LegendCategories != null) { @@ -208,7 +209,7 @@ protected override void ProcessRecord() } else { - throw new Exception("EnableCustomColorPalette requires CustomColorPalette to be set."); + throw new InvalidOperationException("EnableCustomColorPalette requires CustomColorPalette to be set."); } } else diff --git a/Sources/SignalChart.cs b/Sources/SignalChart.cs index a59c048..64e2192 100644 --- a/Sources/SignalChart.cs +++ b/Sources/SignalChart.cs @@ -41,7 +41,7 @@ public object Chart(List values, string[] labels, double xOffset = 0, } else { - throw new Exception("CustomColorPalette is empty. Please provide valid color values."); + throw new InvalidOperationException("CustomColorPalette is empty. Please provide valid color values."); } } else diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 2270be3..9cdd1c0 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -20,7 +20,7 @@ public object Chart(List values, string[] labels, string[] categoryNam } else { - throw new Exception("CustomColorPalette is empty. Please provide valid color values."); + throw new InvalidOperationException("CustomColorPalette is empty. Please provide valid color values."); } } else diff --git a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 index 7c7d375..d8d92dc 100644 --- a/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 +++ b/Tests/AsBuiltReport.Chart.Functions.Tests.ps1 @@ -251,4 +251,48 @@ Describe 'AsBuiltReport.Chart Exported Functions' { } } } + + Context 'Output format file extensions' { + It 'New-PieChart with Format jpg should produce a .jpg file' { + $result = New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'jpg' -OutputFolderPath $TestDrive + $result.Extension | Should -Be '.jpg' + Test-Path $result | Should -BeTrue + } + It 'New-PieChart with Format jpeg should produce a .jpeg file' { + $result = New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'jpeg' -OutputFolderPath $TestDrive + $result.Extension | Should -Be '.jpeg' + Test-Path $result | Should -BeTrue + } + It 'New-BarChart with Format jpg should produce a .jpg file' { + $result = New-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'jpg' -OutputFolderPath $TestDrive + $result.Extension | Should -Be '.jpg' + Test-Path $result | Should -BeTrue + } + It 'New-BarChart with Format jpeg should produce a .jpeg file' { + $result = New-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'jpeg' -OutputFolderPath $TestDrive + $result.Extension | Should -Be '.jpeg' + Test-Path $result | Should -BeTrue + } + } + + Context 'State isolation between cmdlet calls' { + It 'EnableLegend should not persist from one New-PieChart call to the next' { + # First call with legend enabled + New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive -EnableLegend | Out-Null + # Second call without legend - should not throw and should succeed + { New-PieChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + It 'EnableChartBorder should not persist from one New-BarChart call to the next' { + New-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive -EnableChartBorder | Out-Null + { New-BarChart -Title 'Test' -Values @(1, 2) -Labels @('A', 'B') -Format 'png' -OutputFolderPath $TestDrive } | Should -Not -Throw + } + It 'EnableLegend should not persist from New-StackedBarChart to New-BarChart' { + New-StackedBarChart -Title 'Test' -Values @(@(1, 2), @(3, 4)) -Labels @('A', 'B') -LegendCategories @('X', 'Y') -Format 'png' -OutputFolderPath $TestDrive -EnableLegend | Out-Null + { 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 + } + } } From 6ffaab5317b5b6a8d3c8a0cb421f2d97c11afb3a Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Thu, 12 Mar 2026 09:50:41 -0400 Subject: [PATCH 42/62] Revise README with updated links and module details Update README.md to include new links, module information, and examples. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fd24905..568c7b1 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ All examples in the latest release of AsBuiltReport.Chart can be found in the ta | [Example8](./Examples/Example08.ps1) | Signal Chart with DateTime X-Axis (Time-Series Data) | | [Example9](./Examples/Example09.ps1) | Signal Chart with Multiple Lines | -## :watermark: Watermark Support +## Watermark Support All chart types support an optional watermark that overlays semi-transparent text in the center of the chart. The watermark is **disabled by default** and is activated only when the `-EnableWatermark` switch is supplied. @@ -162,3 +162,4 @@ New-SignalChart -Title 'Throughput' -Values @(,[double[]]@(1,2,3,4,5)) -Format ' + From 3e63b467076ce1f23793b633834bf4a76e2eb407 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 09:19:03 -0400 Subject: [PATCH 43/62] Update CHANGELOG for version 0.3.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca6103..4039d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.3.0] - 2026-03-?? +## [0.3.0] - 2026-03-14 ### Added From b9467e5bb70d5092b1710ec3989b60de12a7b597 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:41:56 +0000 Subject: [PATCH 44/62] Initial plan From 2514e07e67fe771ca9774a1c7648161a03c9dc5d Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 10:41:58 -0400 Subject: [PATCH 45/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Sources/BarChart.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/BarChart.cs b/Sources/BarChart.cs index 47cd022..fe9a014 100644 --- a/Sources/BarChart.cs +++ b/Sources/BarChart.cs @@ -200,13 +200,24 @@ public object Chart(double[] values, string[] labels, string filename = "output" // Set axis limits if values are empty or contain only one value to prevent auto-scaling issues if (values.Length == 1) { + double singleValue = values[0]; + double padding = Math.Abs(singleValue) * 0.1; + if (padding == 0) + padding = 1; + if (AreaOrientation == Orientations.Horizontal) { - myPlot.Axes.SetLimits(0, 0, -2, 2); + // Value is on the X-axis for horizontal orientation + double xMin = Math.Min(0, singleValue) - padding; + double xMax = Math.Max(0, singleValue) + padding; + myPlot.Axes.SetLimits(xMin, xMax, -2, 2); } else { - myPlot.Axes.SetLimits(-2, 2, 0, 0); + // Value is on the Y-axis for vertical orientation + double yMin = Math.Min(0, singleValue) - padding; + double yMax = Math.Max(0, singleValue) + padding; + myPlot.Axes.SetLimits(-2, 2, yMin, yMax); } } From bdd5017bd8ed30e4b37ea545db4ff88015092fd1 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 10:42:22 -0400 Subject: [PATCH 46/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 568c7b1..977d720 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ All chart types support an optional watermark that overlays semi-transparent tex | Parameter | Type | Default | Description | |---|---|---|---| | `EnableWatermark` | Switch | (off) | Enables the watermark overlay. | -| `WatermarkText` | String | `AsBuiltReport` | Text to display as the watermark. | +| `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. | @@ -138,7 +138,7 @@ All chart types support an optional watermark that overlays semi-transparent tex ### Watermark Examples ```powershell -# Pie chart with default watermark (gray "AsBuiltReport" at 30% opacity) +# Pie chart with default watermark (gray "Confidential" at 30% opacity) New-PieChart -Title 'Sales' -Values @(10, 20, 30) -Labels @('A', 'B', 'C') -Format 'png' -EnableWatermark # Bar chart with a custom watermark text, color, and opacity From d6b8d144fe7913ed90f712ef9e2d77c5d703ea2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:44:49 +0000 Subject: [PATCH 47/62] Fix null Values handling in New-SignalChart to throw terminating error Co-authored-by: rebelinux <1002783+rebelinux@users.noreply.github.com> --- Sources/PowerShell/SignalChartPwsh.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/PowerShell/SignalChartPwsh.cs b/Sources/PowerShell/SignalChartPwsh.cs index 555c452..972115d 100644 --- a/Sources/PowerShell/SignalChartPwsh.cs +++ b/Sources/PowerShell/SignalChartPwsh.cs @@ -271,7 +271,11 @@ protected override void ProcessRecord() } else { - WriteObject("Values parameter cannot be null or empty."); + ThrowTerminatingError(new ErrorRecord( + new ArgumentException("Values parameter cannot be null or empty."), + "ValuesNullOrEmpty", + ErrorCategory.InvalidArgument, + null)); } } } From da9160d6d21f8b6f0cd77ae3c380302cbd69f57b Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 10:51:12 -0400 Subject: [PATCH 48/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Sources/StackedBarChart.cs | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 9cdd1c0..48f7ed8 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -236,17 +236,44 @@ public object Chart(List values, string[] labels, string[] categoryNam myPlot.DataBackground.Color = GetDrawingColor(DataBackgroundColor.Value); } - // Set axis limits if values are empty or contain only one value to prevent auto-scaling issues - if (values.Count == 1) + // Set axis limits if there is only one value to prevent auto-scaling issues + if (values.Count == 1) { + // Compute the stacked total for the single bar + double stackedTotal = 0; + if (values[0] != null) + { + foreach (double v in values[0]) + { + stackedTotal += v; + } + } + + // Ensure a non-zero span even if stackedTotal is zero or negative + double paddingFraction = 0.1; + double effectiveTotal = stackedTotal > 0 ? stackedTotal * (1 + paddingFraction) : 1.0; + + double xMin, xMax, yMin, yMax; + double barIndex = 0; // single bar at index 0 + if (AreaOrientation == Orientations.Horizontal) { - myPlot.Axes.SetLimits(0, 0, -2, 2); + // Horizontal bars: X is value, Y is category (bar index) + xMin = 0; + xMax = effectiveTotal; + yMin = barIndex - 0.5; + yMax = barIndex + 0.5; } else { - myPlot.Axes.SetLimits(-2, 2, 0, 0); + // Vertical bars: X is category (bar index), Y is value + xMin = barIndex - 0.5; + xMax = barIndex + 0.5; + yMin = 0; + yMax = effectiveTotal; } + + myPlot.Axes.SetLimits(xMin, xMax, yMin, yMax); } // Apply watermark if enabled From 1af2603c1706b5ee0b8292b6930b7556d26129b3 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 10:51:33 -0400 Subject: [PATCH 49/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Examples/Example05.ps1 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Examples/Example05.ps1 b/Examples/Example05.ps1 index 16e06e9..b74b13c 100644 --- a/Examples/Example05.ps1 +++ b/Examples/Example05.ps1 @@ -30,8 +30,10 @@ $OutputFolderPath = Resolve-Path $Path Define the data to be displayed in the chart. For a Stacked Bar Chart: - - $Values is an array of double arrays. Each inner array contains the values for one category - (stack segment) across all bars. + - $Values is an array of double arrays. Each inner array represents one bar and contains the + values for all stack segments (categories) for that bar, in the same order as $LegendCategories. + The number of inner arrays should match $Labels.Length, and the length of each inner array + should match $LegendCategories.Length. - $Labels contains the label for each bar (X-axis entries). - $LegendCategories contains the label for each series (stack segments shown in the legend). @@ -46,16 +48,19 @@ $ChartTitle = 'Datastore Capacity (GB)' $Labels = @('datastore-01', 'datastore-02', 'datastore-03', 'datastore-04') $LegendCategories = @('Used Space', 'Free Space') -# Each inner array represents one category's values across all bars. +# Each inner array represents one bar's segment values, ordered by $LegendCategories. $Values = @(@(800, 200), @(600, 400), @(1500, 500), @(300, 700)) <# The New-StackedBarChart cmdlet generates the Stacked Bar Chart image. -Title : Sets the chart title displayed at the top of the image. - -Values : Array of double arrays. Each inner array is one stack segment across all bars. + -Values : Array of double arrays. Each inner array represents one bar and contains + the values for each stack segment (category) for that bar, ordered to match + $LegendCategories. The number of inner arrays should equal the number of + $Labels. -Labels : Array of label strings, one per bar (X-axis entries). - -LegendCategories : Array of category names shown in the legend (one per inner array in Values). + -LegendCategories : Array of category names shown in the legend (one per value position in each inner array). -EnableLegend : Enables the legend on the chart. -LabelXAxis : Label for the X-axis. -LabelYAxis : Label for the Y-axis. From e10ed62569abc16e6cfddcbd241558d40da0774f Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 10:51:49 -0400 Subject: [PATCH 50/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Examples/Example06.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Examples/Example06.ps1 b/Examples/Example06.ps1 index d8d1cb0..1d01192 100644 --- a/Examples/Example06.ps1 +++ b/Examples/Example06.ps1 @@ -36,8 +36,8 @@ $OutputFolderPath = Resolve-Path $Path Define the data to be displayed in the chart. For a Stacked Bar Chart: - - $Values is an array of double arrays. Each inner array contains the values for one category - (stack segment) across all bars. + - $Values is an array of double arrays. Each inner array contains the values for all categories + (stack segments) for a single bar. - $Labels contains the label for each bar (X-axis entries). - $LegendCategories contains the label for each series (stack segments shown in the legend). @@ -48,7 +48,8 @@ $ChartTitle = 'Network Adapter Traffic (Mbps)' $Labels = @('vmnic0', 'vmnic1', 'vmnic2', 'vmnic3') $LegendCategories = @('Inbound', 'Outbound', 'Dropped') -# Values are organized by category (stack segment), so each inner array corresponds to one category across all bars. +# Values are organized by bar (network adapter), so each inner array corresponds to one adapter and +# contains the values for each traffic type (stack segment) in the order of $LegendCategories. # Example values for Inbound, Outbound, and Dropped traffic for each network adapter: # - vmnic0: 450 Mbps Inbound, 320 Mbps Outbound, 280 Mbps Dropped $vmnic0 = [double[]]@(450, 320, 280) From 73d35a561cd47869343c9b1a91006f6766f07332 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 10:52:01 -0400 Subject: [PATCH 51/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 977d720..b27fe1f 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ New-BarChart -Title 'Test' -Values @(1,2) -Labels @('A','B') -Format 'png' -Enab ### Stacked Bar Chart ```powershell -# Generate a Stacked Bar Chart with the title 'Test', values of 1 and 2 for the first category and 3 and 4 for the second category, labels 'A' and 'B', legend categories 'Value1' and 'Value2', 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. +# Generate a Stacked Bar Chart with the title 'Test', values @(1,2) for bar 'A' and @(3,4) 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-StackedBarChart -Title 'Test' -Values @(@(1,2),@(3,4)) -Labels @('A','B') -LegendCategories @('Value1','Value2') -Format 'png' -EnableLegend -LegendOrientation Horizontal -LegendAlignment UpperCenter -Width 600 -Height 400 -TitleFontSize 20 -LabelFontSize 16 -AxesMarginsTop 1 ``` ![StackedBarChart](./Samples/StackedBarChart.png) From f5748296845f1767ba745106be46bf1838585d0e Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:15:45 -0400 Subject: [PATCH 52/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Sources/PowerShell/SignalChartPwsh.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/PowerShell/SignalChartPwsh.cs b/Sources/PowerShell/SignalChartPwsh.cs index 972115d..78c70bf 100644 --- a/Sources/PowerShell/SignalChartPwsh.cs +++ b/Sources/PowerShell/SignalChartPwsh.cs @@ -272,10 +272,10 @@ protected override void ProcessRecord() else { ThrowTerminatingError(new ErrorRecord( - new ArgumentException("Values parameter cannot be null or empty."), + new ArgumentNullException(nameof(Values), "Values parameter cannot be null or empty."), "ValuesNullOrEmpty", ErrorCategory.InvalidArgument, - null)); + nameof(Values))); } } } From 51c7c26265523559c13f17844a4e52661af83ba8 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:56:37 -0400 Subject: [PATCH 53/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b27fe1f..89ab0de 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The AsBuiltReport Chart supports the following languages; ## :wrench: System Requirements -PowerShell 5.1 or PowerShell 7, and the following PowerShell modules are required for generating a AsBuiltReport Chart. +PowerShell 5.1 or PowerShell 7, and the following PowerShell modules are required for generating an AsBuiltReport Chart. - [AsBuiltReport.Core Module](https://www.powershellgallery.com/packages/AsBuiltReport.Core/) From 371f67f0ffb4934cc74558319a4fee4baf199614 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:57:04 -0400 Subject: [PATCH 54/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Examples/Example09.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example09.ps1 b/Examples/Example09.ps1 index ede1fe8..5ed93a6 100644 --- a/Examples/Example09.ps1 +++ b/Examples/Example09.ps1 @@ -19,7 +19,7 @@ [CmdletBinding()] param ( - [System.IO.FileInfo] $Path = (Get-Location).Path, + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, [string] $Format = 'png' ) From ec90ab5f09f62c2372731f2067f54fd83d6b8d80 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:57:27 -0400 Subject: [PATCH 55/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Examples/Example08.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example08.ps1 b/Examples/Example08.ps1 index b820be2..b066376 100644 --- a/Examples/Example08.ps1 +++ b/Examples/Example08.ps1 @@ -15,7 +15,7 @@ [CmdletBinding()] param ( - [System.IO.FileInfo] $Path = (Get-Location).Path, + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, [string] $Format = 'png' ) From 19e307bdcacb39e4fff61c651345477f32e26769 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:57:41 -0400 Subject: [PATCH 56/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Examples/Example07.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example07.ps1 b/Examples/Example07.ps1 index df685bf..3564272 100644 --- a/Examples/Example07.ps1 +++ b/Examples/Example07.ps1 @@ -12,7 +12,7 @@ [CmdletBinding()] param ( - [System.IO.FileInfo] $Path = (Get-Location).Path, + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, [string] $Format = 'png' ) From 6e8e05a427436fecd99c1cf7231567fcda783419 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:58:19 -0400 Subject: [PATCH 57/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Examples/Example06.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example06.ps1 b/Examples/Example06.ps1 index 1d01192..28cad59 100644 --- a/Examples/Example06.ps1 +++ b/Examples/Example06.ps1 @@ -15,7 +15,7 @@ [CmdletBinding()] param ( - [System.IO.FileInfo] $Path = (Get-Location).Path, + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, [string] $Format = 'png' ) From af72233cad05dd038be57898bea1a796edfc78b4 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:58:31 -0400 Subject: [PATCH 58/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Examples/Example05.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example05.ps1 b/Examples/Example05.ps1 index b74b13c..6d658df 100644 --- a/Examples/Example05.ps1 +++ b/Examples/Example05.ps1 @@ -9,7 +9,7 @@ [CmdletBinding()] param ( - [System.IO.FileInfo] $Path = (Get-Location).Path, + [System.IO.DirectoryInfo] $Path = Get-Location, [string] $Format = 'png' ) From a8ba6c06b4ffae0bd0f1a6a74506f0482bca2a7f Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:58:42 -0400 Subject: [PATCH 59/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Examples/Example02.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example02.ps1 b/Examples/Example02.ps1 index 07d5c1c..7d7af8f 100644 --- a/Examples/Example02.ps1 +++ b/Examples/Example02.ps1 @@ -15,7 +15,7 @@ [CmdletBinding()] param ( - [System.IO.FileInfo] $Path = (Get-Location).Path, + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, [string] $Format = 'png' ) From b637779048992e65239154d0700b8bd95c68470e Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:58:55 -0400 Subject: [PATCH 60/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Examples/Example01.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example01.ps1 b/Examples/Example01.ps1 index accd97e..c460c65 100644 --- a/Examples/Example01.ps1 +++ b/Examples/Example01.ps1 @@ -9,7 +9,7 @@ [CmdletBinding()] param ( - [System.IO.FileInfo] $Path = (Get-Location).Path, + [System.IO.DirectoryInfo] $Path = (Get-Location).Path, [string] $Format = 'png' ) From 904eae5ac2c6b75c5615a26ba6f2a494942cee3f Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:59:43 -0400 Subject: [PATCH 61/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Sources/StackedBarChart.cs | 48 +++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/Sources/StackedBarChart.cs b/Sources/StackedBarChart.cs index 48f7ed8..47f4968 100755 --- a/Sources/StackedBarChart.cs +++ b/Sources/StackedBarChart.cs @@ -239,19 +239,45 @@ public object Chart(List values, string[] labels, string[] categoryNam // Set axis limits if there is only one value to prevent auto-scaling issues if (values.Count == 1) { - // Compute the stacked total for the single bar - double stackedTotal = 0; - if (values[0] != null) + // Compute cumulative sums for the single stacked bar to capture negative and positive extents + double cumulative = 0; + double minSum = 0; + double maxSum = 0; + + if (values[0] != null && values[0].Length > 0) { foreach (double v in values[0]) { - stackedTotal += v; + cumulative += v; + if (cumulative < minSum) + { + minSum = cumulative; + } + if (cumulative > maxSum) + { + maxSum = cumulative; + } } } - // Ensure a non-zero span even if stackedTotal is zero or negative - double paddingFraction = 0.1; - double effectiveTotal = stackedTotal > 0 ? stackedTotal * (1 + paddingFraction) : 1.0; + // Ensure that zero is included in the value axis and add padding + double minVal = Math.Min(0, minSum); + double maxVal = Math.Max(0, maxSum); + + if (minVal == maxVal) + { + // Degenerate case: all segments sum to the same value (including all zero) + double pad = (minVal == 0) ? 1.0 : Math.Abs(minVal) * 0.1; + minVal -= pad; + maxVal += pad; + } + else + { + double range = maxVal - minVal; + double pad = range * 0.1; + minVal -= pad; + maxVal += pad; + } double xMin, xMax, yMin, yMax; double barIndex = 0; // single bar at index 0 @@ -259,8 +285,8 @@ public object Chart(List values, string[] labels, string[] categoryNam if (AreaOrientation == Orientations.Horizontal) { // Horizontal bars: X is value, Y is category (bar index) - xMin = 0; - xMax = effectiveTotal; + xMin = minVal; + xMax = maxVal; yMin = barIndex - 0.5; yMax = barIndex + 0.5; } @@ -269,8 +295,8 @@ public object Chart(List values, string[] labels, string[] categoryNam // Vertical bars: X is category (bar index), Y is value xMin = barIndex - 0.5; xMax = barIndex + 0.5; - yMin = 0; - yMax = effectiveTotal; + yMin = minVal; + yMax = maxVal; } myPlot.Axes.SetLimits(xMin, xMax, yMin, yMax); From 907aa7d83c5261937dc090e78547adde1f3118c0 Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Sat, 14 Mar 2026 11:59:52 -0400 Subject: [PATCH 62/62] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Sources/BarChart.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/BarChart.cs b/Sources/BarChart.cs index fe9a014..5002f17 100644 --- a/Sources/BarChart.cs +++ b/Sources/BarChart.cs @@ -210,14 +210,16 @@ public object Chart(double[] values, string[] labels, string filename = "output" // Value is on the X-axis for horizontal orientation double xMin = Math.Min(0, singleValue) - padding; double xMax = Math.Max(0, singleValue) + padding; - myPlot.Axes.SetLimits(xMin, xMax, -2, 2); + // Use ±0.5 on the category (Y) axis to match bar index spacing for a single bar + myPlot.Axes.SetLimits(xMin, xMax, -0.5, 0.5); } else { // Value is on the Y-axis for vertical orientation double yMin = Math.Min(0, singleValue) - padding; double yMax = Math.Max(0, singleValue) + padding; - myPlot.Axes.SetLimits(-2, 2, yMin, yMax); + // Use ±0.5 on the category (X) axis to match bar index spacing for a single bar + myPlot.Axes.SetLimits(-0.5, 0.5, yMin, yMax); } }