Plugins can render an email-safe visualization on top of their table/list. It is opt-in, additive, and theme-independent: every chart resolves its colours and fonts from the active theme tokens, so all 8 themes inherit it with zero per-theme code.
A plugin sets $Chart (a hashtable) alongside its usual $Display:
$Display = "Table"
$Chart = @{ Type = "Donut"; Group = "Compliance" }The engine (vCheck.ps1) passes the plugin's raw row objects (the same data your table is
built from) to Get-HTMLChart and prepends the result above the rendered table/list. The
chart never replaces the detail; it summarises it. If $Chart is unset, nothing changes.
Implementation lives in Charts.ps1. Donut/Pie are rasterised to a PNG entirely
in PowerShell 7: no System.Drawing (Windows-only), no native libraries, no NuGet; just the
BCL plus a hand-rolled CRC32/Adler32. The PNG is embedded through the existing
Add-ReportResource (CID) path, so it renders in every mail client and the standalone .htm.
The bar/stacked/gauge/heat/sparkline types are pure table-based HTML.
Type |
Shape | Email-safe | Best for |
|---|---|---|---|
Donut |
ring + centre total + legend (PNG) | ✅ all clients | composition: compliant vs not, severity mix |
Pie |
full pie + legend (PNG) | ✅ all clients | composition where the total isn't the headline |
Bar |
ranked horizontal bars | ✅ all clients | magnitude: utilisation %, top-N offenders |
Stacked |
one bar split crit/warn/ok | ✅ all clients | a single at-a-glance severity split |
Gauge |
labelled meter(s) vs thresholds | ✅ all clients | one or a few headline values |
Heat |
grid of status cells | ✅ all clients | per-entity status at scale (one cell per host/VM) |
Sparkline |
mini column trend | a trend (events/day); cell heights wobble in old Outlook |
All keys name a property on your row objects unless noted.
# Composition: counts rows per distinct value of Group, colours by keyword
@{ Type = "Donut"; Group = "Compliance" } # or Type = "Pie"
# ...or supply explicit segments:
@{ Type = "Donut"; Segments = @(@{label='OK';value=28;color=$cOk}, ...) }
# Magnitude: ranked bars, coloured by threshold (higher = worse unless Invert)
@{ Type = "Bar"; Label = "Name"; Value = "UsedPct"; Max = 100; Warn = 75; Crit = 90; Top = 12 }
# Severity split: from a Group column (keyword-mapped) or explicit counts
@{ Type = "Stacked"; Group = "Status" }
@{ Type = "Stacked"; Critical = 3; Warning = 5; OK = 28 }
# Headline values vs thresholds (one meter per row)
@{ Type = "Gauge"; Label = "Cluster"; Value = "MemPct"; Warn = 75; Crit = 90 } # Invert = $true for "free %"
# Per-entity status (one cell per row)
@{ Type = "Heat"; Status = "State"; PerRow = 24 }
# Trend (bars in row order)
@{ Type = "Sparkline"; Value = "Count" }Group/Status values are matched to theme severity tokens by keyword (case-insensitive):
crit|fail|error|non-compliant|down|expired|disabled|… → critical;
warn|degraded|aging|stale|pending|partial|… → warning;
ok|compliant|healthy|pass|connected|enabled|secure|… → OK.
Non-severity categories cycle an accent palette. For Bar/Gauge, colour comes from the
numeric Warn/Crit thresholds instead (set Invert = $true when lower is worse).
- A chart should summarise data the plugin already returns. Reuse the row objects; don't
re-query. Add a computed column (e.g. a numeric
UsedPct) if the viz needs one. - Charts suit aggregate / composition / utilisation plugins. A plugin that just lists a few problem items is usually clearest as a plain table; not every plugin needs a chart.
- Keep
$Display = "Table"(or"List") so the detail stays beneath the chart. - Donut/Pie cost ~0.9 s to rasterise, so use them on summary plugins, not per-item ones.