Skip to content

Commit f2c2fb4

Browse files
committed
Add Vega-Lite HTML reports article
1 parent 8c4d6f5 commit f2c2fb4

3 files changed

Lines changed: 324 additions & 0 deletions

File tree

‎content/articles/2026/10/_index.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: "Articles from October 2026"
3+
description: "PowerShell.org Articles published in October 2026."
4+
---
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
---
2+
title: "Build Vega-Lite HTML Reports with PowerShell"
3+
description: "Use PowerShell objects and Vega-Lite specifications to generate an interactive HTML report without adopting a separate reporting framework."
4+
author: Andrey Vernigora
5+
authors:
6+
- Andrey Vernigora
7+
date: "2026-10-05T00:00:00+00:00"
8+
categories:
9+
- PowerShell for Developers
10+
tags:
11+
- powershell
12+
- vega-lite
13+
- data-visualization
14+
- html
15+
- reporting
16+
---
17+
18+
PowerShell is good at acquiring data and turning it into objects. Vega-Lite is good at turning structured data into interactive graphics. A small function is enough to connect the two without adopting a reporting framework or opening a browser from the script.
19+
20+
In this article, we download the [Palmer Penguins](https://allisonhorst.github.io/palmerpenguins/) dataset, clean it with ordinary PowerShell, describe three views as one Vega-Lite specification, and save the finished report as an HTML file.
21+
22+
The key design choice is that `Show-VegaLite` doesn't decide what to do with the HTML. It returns the document to the pipeline:
23+
24+
```powershell
25+
$reportSpec |
26+
Show-VegaLite -PageTitle 'Palmer Penguins report' |
27+
Set-Content -Path ./penguins-report.html -Encoding utf8
28+
```
29+
30+
That makes the renderer useful in a console script, scheduled job, CI pipeline, or notebook.
31+
32+
## The finished report
33+
34+
The example produces one HTML document containing a bar chart, scatter plot, and box plot.
35+
36+
![Palmer Penguins HTML report generated from PowerShell objects with Vega-Lite](palmer-penguins-report.png)
37+
38+
The charts answer progressively richer questions:
39+
40+
- How many complete observations are available for each species?
41+
- How do bill length and bill depth separate the species?
42+
- How different are the body-mass distributions?
43+
44+
The saved report remains interactive. Vega-Embed provides tooltips and an action menu that can export individual views.
45+
46+
## Load and shape the data
47+
48+
The simplified Palmer Penguins dataset has 344 rows and eight columns. It contains categories, measurements, missing values, and several visible relationships, which makes it a useful alternative to the classic Iris dataset. The project publishes the data under CC0 and documents the original Palmer Station LTER sources.
49+
50+
PowerShell can download the CSV directly:
51+
52+
```powershell
53+
$dataUrl = 'https://raw.githubusercontent.com/' +
54+
'allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv'
55+
56+
$penguins = Invoke-RestMethod -Uri $dataUrl |
57+
ConvertFrom-Csv |
58+
Where-Object {
59+
$_.bill_length_mm -ne 'NA' -and
60+
$_.bill_depth_mm -ne 'NA' -and
61+
$_.flipper_length_mm -ne 'NA' -and
62+
$_.body_mass_g -ne 'NA' -and
63+
$_.sex -ne 'NA'
64+
} |
65+
ForEach-Object {
66+
[pscustomobject]@{
67+
species = $_.species
68+
island = $_.island
69+
bill_length_mm = [double]$_.bill_length_mm
70+
bill_depth_mm = [double]$_.bill_depth_mm
71+
flipper_length_mm = [int]$_.flipper_length_mm
72+
body_mass_g = [int]$_.body_mass_g
73+
sex = $_.sex
74+
year = [int]$_.year
75+
}
76+
}
77+
```
78+
79+
The explicit casts matter. `ConvertFrom-Csv` initially creates strings, while Vega-Lite should receive JSON numbers for quantitative fields. Removing incomplete records keeps this example focused. A production report could retain them and add a separate data-quality summary.
80+
81+
## Describe several charts in one specification
82+
83+
Vega-Lite specifications are JSON documents. PowerShell ordered hashtables and arrays let us construct the same structure while keeping the data as objects until the final serialization step.
84+
85+
The top-level `data` property makes the cleaned data available to every view. `vconcat` places the count chart above an `hconcat` containing the scatter and box plots:
86+
87+
```powershell
88+
$reportSpec = [ordered]@{
89+
'$schema' = 'https://vega.github.io/schema/vega-lite/v6.json'
90+
data = @{ values = @($penguins) }
91+
spacing = 24
92+
vconcat = @(
93+
@{
94+
width = 760
95+
height = 150
96+
title = 'Observations by species'
97+
mark = @{ type = 'bar'; cornerRadiusEnd = 3 }
98+
encoding = @{
99+
x = @{
100+
field = 'species'
101+
type = 'nominal'
102+
title = $null
103+
sort = '-y'
104+
}
105+
y = @{
106+
aggregate = 'count'
107+
type = 'quantitative'
108+
title = 'Penguins'
109+
}
110+
color = @{
111+
field = 'species'
112+
type = 'nominal'
113+
legend = $null
114+
}
115+
tooltip = @(
116+
@{ field = 'species'; type = 'nominal'; title = 'Species' }
117+
@{ aggregate = 'count'; type = 'quantitative'; title = 'Observations' }
118+
)
119+
}
120+
}
121+
@{
122+
hconcat = @(
123+
@{
124+
width = 365
125+
height = 300
126+
title = 'Bill dimensions'
127+
mark = @{
128+
type = 'point'
129+
filled = $true
130+
opacity = 0.72
131+
size = 65
132+
}
133+
encoding = @{
134+
x = @{
135+
field = 'bill_length_mm'
136+
type = 'quantitative'
137+
title = 'Bill length (mm)'
138+
scale = @{ zero = $false }
139+
}
140+
y = @{
141+
field = 'bill_depth_mm'
142+
type = 'quantitative'
143+
title = 'Bill depth (mm)'
144+
scale = @{ zero = $false }
145+
}
146+
color = @{
147+
field = 'species'
148+
type = 'nominal'
149+
title = 'Species'
150+
}
151+
shape = @{
152+
field = 'sex'
153+
type = 'nominal'
154+
title = 'Sex'
155+
}
156+
}
157+
}
158+
@{
159+
width = 365
160+
height = 300
161+
title = 'Body mass distribution'
162+
mark = @{
163+
type = 'boxplot'
164+
extent = 'min-max'
165+
size = 34
166+
}
167+
encoding = @{
168+
x = @{
169+
field = 'species'
170+
type = 'nominal'
171+
title = $null
172+
}
173+
y = @{
174+
field = 'body_mass_g'
175+
type = 'quantitative'
176+
title = 'Body mass (g)'
177+
scale = @{ zero = $false }
178+
}
179+
color = @{
180+
field = 'species'
181+
type = 'nominal'
182+
legend = $null
183+
}
184+
}
185+
}
186+
)
187+
}
188+
)
189+
config = @{
190+
view = @{ stroke = $null }
191+
axis = @{
192+
labelColor = '#42506a'
193+
titleColor = '#27334a'
194+
gridColor = '#e6eaf0'
195+
}
196+
title = @{
197+
anchor = 'start'
198+
color = '#17233c'
199+
fontSize = 16
200+
}
201+
range = @{
202+
category = @('#4c78a8', '#f58518', '#54a24b')
203+
}
204+
}
205+
}
206+
```
207+
208+
This is still only data. No chart process has started, and PowerShell hasn't emitted HTML yet.
209+
210+
## Return HTML instead of taking control
211+
212+
`Show-VegaLite` performs four operations:
213+
214+
1. Serialize the specification with enough JSON depth for nested encodings.
215+
2. Encode that JSON as UTF-8 Base64 to avoid quoting and `</script>` problems inside the page.
216+
3. Create a small HTML document that loads pinned Vega, Vega-Lite, and Vega-Embed versions.
217+
4. Return the document as a string.
218+
219+
```powershell
220+
function Show-VegaLite {
221+
[CmdletBinding()]
222+
param(
223+
[Parameter(Mandatory, ValueFromPipeline)]
224+
[System.Collections.IDictionary]$Spec,
225+
226+
[string]$PageTitle = 'Vega-Lite report',
227+
228+
[ValidateSet('svg', 'canvas')]
229+
[string]$Renderer = 'svg'
230+
)
231+
232+
process {
233+
$specJson = $Spec | ConvertTo-Json -Depth 100 -Compress
234+
$specBase64 = [Convert]::ToBase64String(
235+
[Text.Encoding]::UTF8.GetBytes($specJson)
236+
)
237+
$encodedTitle = [Net.WebUtility]::HtmlEncode($PageTitle)
238+
239+
@"
240+
<!doctype html>
241+
<html lang="en">
242+
<head>
243+
<meta charset="utf-8">
244+
<meta name="viewport" content="width=device-width, initial-scale=1">
245+
<title>$encodedTitle</title>
246+
<script src="https://cdn.jsdelivr.net/npm/vega@6.3.1"></script>
247+
<script src="https://cdn.jsdelivr.net/npm/vega-lite@6.4.3"></script>
248+
<script src="https://cdn.jsdelivr.net/npm/vega-embed@7.1.0"></script>
249+
<style>
250+
body { font-family: system-ui, sans-serif; margin: 2rem; }
251+
#vis { overflow-x: auto; }
252+
.error { color: #a61b1b; white-space: pre-wrap; }
253+
</style>
254+
</head>
255+
<body>
256+
<h1>$encodedTitle</h1>
257+
<div id="vis"></div>
258+
<script>
259+
const binary = atob("$specBase64");
260+
const bytes = Uint8Array.from(
261+
binary,
262+
character => character.charCodeAt(0)
263+
);
264+
const spec = JSON.parse(new TextDecoder().decode(bytes));
265+
266+
vegaEmbed("#vis", spec, {
267+
mode: "vega-lite",
268+
renderer: "$Renderer",
269+
actions: true
270+
}).catch(error => {
271+
const message = document.createElement("div");
272+
message.className = "error";
273+
message.textContent = error.stack || error.message;
274+
document.querySelector("#vis").replaceChildren(message);
275+
});
276+
</script>
277+
</body>
278+
</html>
279+
"@
280+
}
281+
}
282+
```
283+
284+
The function doesn't call `Set-Content`, `Out-File`, `Start-Process`, or a notebook-specific display command. That separation lets the caller choose the destination.
285+
286+
## Save the report
287+
288+
Pipe the specification through the renderer and save the returned string:
289+
290+
```powershell
291+
$outputPath = Join-Path $PWD 'penguins-report.html'
292+
293+
$reportSpec |
294+
Show-VegaLite -PageTitle 'Palmer Penguins report' |
295+
Set-Content -Path $outputPath -Encoding utf8
296+
297+
Get-Item $outputPath
298+
```
299+
300+
The resulting file contains the selected data and complete Vega-Lite specification. You can attach it to a ticket, publish it as a build artifact, copy it to static hosting, or open it locally.
301+
302+
It isn't completely offline: the HTML contains the data and chart definition, but it loads the JavaScript runtimes from jsDelivr. A fully offline variant can download those runtime files and reference local copies, at the cost of shipping several additional assets.
303+
304+
## Reuse the same output elsewhere
305+
306+
Returning HTML to the pipeline leaves room for other destinations. For example, a host that supports rich MIME output can render the same document directly:
307+
308+
```powershell
309+
$reportSpec |
310+
Show-VegaLite |
311+
Display -MimeType 'text/html'
312+
```
313+
314+
The boundary stays simple: PowerShell prepares objects, Vega-Lite describes the visualization, and the last command decides whether the result becomes a file, build artifact, web page, or interactive cell.
315+
316+
## References
317+
318+
- [Palmer Penguins project and data documentation](https://allisonhorst.github.io/palmerpenguins/)
319+
- [Vega-Lite documentation](https://vega.github.io/vega-lite/)
320+
- [Vega-Embed documentation](https://github.com/vega/vega-embed)
76.9 KB
Loading

0 commit comments

Comments
 (0)