Playground
A small inline chart drawn as a stepped pixel line with a soft area underneath. Give it a JSON values array, or use push mode: every time the value attribute changes, a point is appended (a starting value is the latest point, after any values). It keeps the last length points: fewer spread across the width, and once it's full the line scrolls. Point value at a signal, and the line follows it as the signal ticks, from the client or from the server.
A point is added when the value changes: the same value again adds nothing (el.push() records a repeat). A removed or empty value attribute adds nothing either, and neither does the same value put back. In a region the server morphs, add data-preserve-attr="value" next to data-attr:value, as in the examples: a morph resets attributes to the server's markup, and a value written there would be a new point.
Examples
Push mode
data-on-interval changes a signal every 400 ms. data-attr pushes each new value into the chart.
<div data-signals:_alt="50" data-on-interval__duration.400ms="$_alt = Math.max(0, Math.min(100, $_alt + (Math.random() * 20 - 9)))">
<sb-sparkline data-attr:value="$_alt" data-preserve-attr="value" show-value tone="accent" length="48" style="--sb-sparkline-width: 20rem"></sb-sparkline>
</div>
Static data
<sb-sparkline values="[3,5,4,8,6,9,12,10,14,13,17]" tone="ok" show-value></sb-sparkline>
<sb-sparkline values="[90,82,85,70,72,60,48,52,40,33,21]" tone="danger" show-value unit="%"></sb-sparkline>
<sb-sparkline values="[40,60,40,60,40,60,40,60]" scale="fixed" min="0" max="100" tone="brand"></sb-sparkline>
Live from the server
The server pushes $_tm.alt four times a second. Push mode turns every change into a new point.
<div data-signals="{_tm: {alt: 0}}">
<div id="telemetry" hidden data-init="@get('/demo/telemetry', {retry: 'error', retryMaxCount: Infinity, retryInterval: 400, retryMaxWait: 4000})"></div>
<sb-sparkline data-attr:value="$_tm.alt" data-preserve-attr="value" length="80" show-value unit=" km" tone="accent" label="Altitude" style="--sb-sparkline-width: 22rem"></sb-sparkline>
</div>
The endpoint is Go with the sse package of lith, Starbase's runtime; any Datastar SDK works the same way. TelemetryAt computes the sample from the clock, so every viewer watches the same flight. When the server stops for a deploy, its last event replaces the hidden element by a copy, which opens the stream again, and retry: 'error' keeps asking until the next process answers:
// demoTelemetry streams the demo mission as signal patches ($_tm): a pure
// query stream, no commands, no database. When the server stops (a deploy),
// the last event asks the page to open it again from #telemetry.
func (s *Server) demoTelemetry(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*") // public; used from the playground sandbox
if s.ctx.Err() != nil {
// Stopping: the page retries the 503 until the next process answers.
http.Error(w, "the server is restarting", http.StatusServiceUnavailable)
return
}
// Uncompressed: an event is 130 bytes, where an encoder would hold 430 KiB
// or more for as long as a page shows the telemetry.
stream, err := sse.Open(w, r, sse.Options{NoCompression: true})
if err != nil {
return
}
var last [][]byte
defer func() { stream.CloseWith(last...) }()
tick := time.NewTicker(250 * time.Millisecond)
defer tick.Stop()
for {
tm, _ := json.Marshal(map[string]any{"_tm": TelemetryAt(time.Now())})
if err := stream.Write(sse.PatchSignals(tm, false)); err != nil {
return
}
select {
case <-tick.C:
case <-r.Context().Done():
return
case <-s.ctx.Done():
last = append(last, sse.Reconnect(ui.TelemetryID))
return
}
}
}
From JavaScript
el.push(n) appends a point, and el.data returns a copy of the buffer. The buffer survives moving the element in the page.
Styling
Style it from your page's CSS, without changing the component or importing anything into it. Custom properties, inherited properties and ::part() all reach into its shadow root.
- Size:
--sb-sparkline-width(default12rem) is the width of the line and the value together, and--sb-sparkline-height(default2.25rem) the height of the line. Its pixels stay square: the line is 24 of them high and as many wide as fit. - Fonts: the value uses your page's font.
- Colours:
tonepicks the line's token:brandis--sb-brand-light,ok--sb-ok,warn--sb-warn,danger--sb-danger,accent--sb-accent. The line repaints whenever that colour changes: a theme switch, the system's light or dark mode, or your own class that redefines the token. The value is--sb-text-1. - 8-bit details: at
--sb-notch: 0(data-sb-style="smooth") the line runs straight from point to point instead of in steps, drawn smoothly at the screen's resolution, and it redraws when the token changes. - Parts:
line(the canvas) andvalue. Your page's::part()rules win over the component's own, without!important.
<style>
.my-spark { --sb-sparkline-width: 16rem; --sb-sparkline-height: 3rem; --sb-brand-light: #22C55E; }
.my-spark::part(value) { font-size: 1.125rem; }
</style>
<sb-sparkline class="my-spark" values="[12,18,15,22,30,26,34,41]" show-value unit="%"></sb-sparkline>
Accessibility
The canvas is role="img", and its label summarises the data (point count, latest, low and high). Name what it shows with label, which comes first ("Altitude: Trend of 80 points: …"): an aria-label on the element itself doesn't reach the image. The shown value is hidden from screen readers, since the label has it. Use show-value when the latest number matters.
Installation
Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag: the autoloader imports each component the first time its tag appears, including tags a Datastar morph adds later. The Datastar here is v1.0.4 with Starbase's fixes until they are released; the components also run on the official release.
<script type="importmap">
{ "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<sb-sparkline values="[12,18,15,22,30,26,34,41]" show-value unit="%"></sb-sparkline>No flash of undefined elements: put class="sb-cloak" on <html> and add .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the page's components are defined.
Load just this component, pinned to this version: all its modules in one minified file with its integrity hash, so the browser refuses it if a single byte changes.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/sparkline@a0cf9e5049ff/sparkline.bundle.min.js" integrity="sha384-qrg61oIcD6leDgXrvpfUcvhufUhMb9Auw5EEXDEbN+RBVb0Irvq3Win5KJhvcqmw"></script>
<sb-sparkline values="[12,18,15,22,30,26,34,41]" show-value unit="%"></sb-sparkline>Or the minified module files (one file, which the browser requests as it finds their imports), each pinned in the import map:
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/sparkline@a0cf9e5049ff/sparkline.min.js": "sha384-/jyIjww9AcSZpZed479Uj4cAu0GUmIWRULutR1RD7onYUrLBy7XSQFTdC0cp6raQ"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/sparkline@a0cf9e5049ff/sparkline.min.js" integrity="sha384-/jyIjww9AcSZpZed479Uj4cAu0GUmIWRULutR1RD7onYUrLBy7XSQFTdC0cp6raQ"></script>
<sb-sparkline values="[12,18,15,22,30,26,34,41]" show-value unit="%"></sb-sparkline>In production, pin today's whole catalog: its autoloader and an import map with integrity hashes, so the browser refuses any file that changed. This map covers Datastar and this component; importmap.json has the hashes of every component, to merge into yours.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/sparkline@a0cf9e5049ff/sparkline.bundle.min.js": "sha384-qrg61oIcD6leDgXrvpfUcvhufUhMb9Auw5EEXDEbN+RBVb0Irvq3Win5KJhvcqmw"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@339249aca48f/autoloader.js" integrity="sha384-ZNpMayI1XShztIEX0iDHyCDDbhT8H+t+zEESUq4BCrN4b+7m931LQEl5x0vbY+Vo"></script>
<sb-sparkline values="[12,18,15,22,30,26,34,41]" show-value unit="%"></sb-sparkline>Components import only datastar and their own files, so copying is enough: save each component's one file (and a file it loads on first use, keeping its folder), and point the import map at your own copy of datastar-rocket.js (the /js/ paths stand for yours).
<sb-sparkline>sparkline.bundle.min.jsone file 1.8 kB brotli
<script type="importmap">
{ "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/sparkline/sparkline.bundle.min.js"></script>
<sb-sparkline values="[12,18,15,22,30,26,34,41]" show-value unit="%"></sb-sparkline>Or copy the module files, keeping each component's folder, and load each component's module:
<script type="importmap">
{ "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/sparkline/sparkline.min.js"></script>
<sb-sparkline values="[12,18,15,22,30,26,34,41]" show-value unit="%"></sb-sparkline>Size
A page loads this component as one file, its bundle, compressed the way it is served (gzip -9, brotli -11): brotli is what it downloads. Datastar and Rocket are shared by every component and not counted.
| File | Original | gzip | brotli |
|---|---|---|---|
sparkline.bundle.min.js | 3.8 kB | 2.0 kB | 1.8 kB |
As module files: one request, 1.8 kB minified with brotli
For a page that loads the module files as they are, requesting each one as it finds its import, or bundles them itself. Each file compressed on its own; the minified one is what the page downloads, and the readable source is always there too.
| File | Original | gzip | brotli | minified |
|---|---|---|---|---|
sparkline.js | 7.4 kB | 3.2 kB | 2.8 kB | 1.8 kB |
API reference
Props
| Attribute | Type | Default | Description |
|---|---|---|---|
values | array | [] | Initial data points, as a JSON array. |
value | number | 0 | Push mode: every change appends a point. |
length | number | 40 | Points kept (older ones scroll off). |
scale | "auto" | "fixed" | "auto" | auto fits the data; fixed uses min and max. |
min | number | 0 | Bottom of the fixed scale. |
max | number | 100 | Top of the fixed scale. |
tone | "brand" | "ok" | "warn" | "danger" | "accent" | "brand" | Line colour (theme token). |
show-value | boolean | false | Show the latest value next to the line. |
unit | string | "" | Unit after the shown value. |
decimals | number | 0 | Decimals of the shown value. |
label | string | "" | What the chart shows, named first in its accessible label. |