Sparklines
Tiny inline charts from a list of numbers: SVG line and bar sparklines for table cells and sentences, and the same in text (▁▃▆█).
Version 1.0.0 · 3 functions · 0 crates
Functions
What use sparkline.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use sparkline_svg, sparkline_bars_svg from sparkline (Quillo 0.6).
sparkline_svg(values: list, options: any) → stror an errorA line sparkline as SVG, width × height pixels (100 × 24) in options (or null), with color ("#2a78d6", or "currentColor" to take the text's), fill true for a light wash under the line, dot false to leave out the dot on the last value, and min/max to fix the scale (to compare several). Null values leave gaps. Up to 10,000 values.
sparkline_bars_svg(values: list, options: any) → stror an errorA bar sparkline as SVG, the same options but fill and dot; bars rise from 0, those below it in negative ("#e34948"). For wins and losses, daily counts.
sparkline_text(values: list) → stror an errorThe values as a line of block characters, lowest ▁ to highest █, a space for null: for plain-text emails, logs and chat messages.
Get it
$ quillo install sparkline # sparkline.qwer into this folder$ quillo install sparkline --example # and its example, sparkline.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/sparkline.qwer$ curl -O https://quillolang.org/plugins/files/sparkline.qweThen, in your program, all of it or only what you call:
use sparkline.qweruse sparkline_svg, sparkline_bars_svg from sparkline // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A dashboard table with a sparkline in each row, and the same trend in a plain-text email.//// quillo run sparkline.qwe then open http://localhost:8080/use "sparkline.qwer"let shops = [ { name: "Lyon", week: [120, 135, 128, 150, 172, 165, 190], profit: [12, 8, -3, 15, 20, -6, 18] }, { name: "Nantes", week: [90, 95, 110, 105, null, 130, 138], profit: [5, -2, 7, 9, 3, 4, 11] },]route GET / = <html><body style="font-family: sans-serif"> <table> <tr><th>Shop</th><th>Orders this week</th><th>Profit by day</th></tr> {for s in shops => <tr> <td>{s.name}</td> <td>{raw(sparkline_svg(s.week, { width: 120, height: 28, fill: true, min: 0, max: 200 })?)} {last(s.week)}</td> <td>{raw(sparkline_bars_svg(s.profit, { width: 84, height: 28 })?)}</td> </tr>} </table></body></html>route GET /email = join(shops | map((s) => `${s.name}: ${sparkline_text(s.week)?}`), "\n")route GET /lyon.svg = respond(200, sparkline_svg(shops[0].week, null)?, { "content-type": "image/svg+xml" })$ quillo run sparkline.qwe$ curl 'localhost:8080/email'$ curl 'localhost:8080/lyon.svg'$ curl 'localhost:8080/'Source
//! Tiny inline charts from a list of numbers: SVG sparklines and bar sparklines to sit in a table cell or a sentence, and the same in text (▁▃▆█).//! version: 1.0.0//!//! [dependencies]use serde_json::Value;const MAX_POINTS: usize = 10_000;fn colour(o: &Value, key: &str, d: &str) -> String { match o.get(key).and_then(Value::as_str) { Some(c) if (c.starts_with('#') && [4, 7, 9].contains(&c.len()) && c[1..].chars().all(|x| x.is_ascii_hexdigit())) || c == "currentColor" => c.to_string(), _ => d.to_string(), }}fn size(o: &Value, key: &str, d: f64) -> f64 { o.get(key).and_then(Value::as_f64).unwrap_or(d).clamp(4.0, 2000.0)}fn clean(values: &[Value]) -> Result<Vec<Option<f64>>, String> { if values.len() > MAX_POINTS { return Err(format!("up to {} values", MAX_POINTS)); } Ok(values.iter().map(|v| v.as_f64().filter(|x| x.is_finite())).collect())}fn range(v: &[Option<f64>], o: &Value) -> (f64, f64) { let vals: Vec<f64> = v.iter().flatten().copied().collect(); let lo = o.get("min").and_then(Value::as_f64).unwrap_or_else(|| vals.iter().cloned().fold(f64::INFINITY, f64::min)); let hi = o.get("max").and_then(Value::as_f64).unwrap_or_else(|| vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max)); if !lo.is_finite() || !hi.is_finite() { (0.0, 1.0) } else if hi - lo < 1e-12 { (lo - 1.0, hi + 1.0) } else { (lo, hi) }}/// A line sparkline as SVG, `width` × `height` pixels (100 × 24) in `options` (or null), with `color` ("#2a78d6", or "currentColor" to take the text's),/// `fill` true for a light wash under the line, `dot` false to leave out the dot on the last value, and `min`/`max` to fix the scale (to compare several)./// Null values leave gaps. Up to 10,000 values.pub fn sparkline_svg(values: Vec<serde_json::Value>, options: serde_json::Value) -> Result<String, String> { let v = clean(&values)?; let (w, h) = (size(&options, "width", 100.0), size(&options, "height", 24.0)); let col = colour(&options, "color", "#2a78d6"); let (lo, hi) = range(&v, &options); let pad = 3.0; let n = v.len().max(2); let x = |i: usize| pad + (w - 2.0 * pad) * i as f64 / (n - 1) as f64; let y = |val: f64| h - pad - ((val - lo) / (hi - lo)).clamp(0.0, 1.0) * (h - 2.0 * pad); let mut d = String::new(); let mut pen = false; for (i, val) in v.iter().enumerate() { match val { Some(val) => { d += &format!("{}{:.1} {:.1}", if pen { "L" } else { "M" }, x(i), y(*val)); pen = true; } None => pen = false, } } let mut out = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}" role="img" style="vertical-align: middle">"#); let pts: Vec<(f64, f64)> = v.iter().enumerate().filter_map(|(i, val)| val.map(|val| (x(i), y(val)))).collect(); if options.get("fill").and_then(Value::as_bool) == Some(true) { if let (Some(a), Some(b)) = (pts.first(), pts.last()) { let body: String = pts.iter().map(|(px, py)| format!("L{:.1} {:.1}", px, py)).collect(); out += &format!(r#"<path d="M{:.1} {:.1}{}L{:.1} {:.1}Z" fill="{col}" fill-opacity="0.12"/>"#, a.0, h - pad, body, b.0, h - pad); } } out += &format!(r#"<path d="{d}" fill="none" stroke="{col}" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>"#); if options.get("dot").and_then(Value::as_bool) != Some(false) { if let Some((px, py)) = pts.last() { out += &format!(r#"<circle cx="{px:.1}" cy="{py:.1}" r="2.5" fill="{col}"/>"#); } } out += "</svg>"; Ok(out)}/// A bar sparkline as SVG, the same `options` but `fill` and `dot`; bars rise from 0, those below it in `negative` ("#e34948"). For wins and losses, daily counts.pub fn sparkline_bars_svg(values: Vec<serde_json::Value>, options: serde_json::Value) -> Result<String, String> { let v = clean(&values)?; let (w, h) = (size(&options, "width", 100.0), size(&options, "height", 24.0)); let col = colour(&options, "color", "#2a78d6"); let neg = colour(&options, "negative", "#e34948"); let (lo, hi) = range(&v, &options); let (lo, hi) = (lo.min(0.0), hi.max(0.0)); let n = v.len().max(1) as f64; let slot = w / n; let bw = (slot - 1.0).max(0.5); let y = |val: f64| h - ((val - lo) / (hi - lo)) * h; let base = y(0.0); let mut out = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}" role="img" style="vertical-align: middle">"#); for (i, val) in v.iter().enumerate() { let Some(val) = val else { continue }; let top = y(*val).min(base); let height = (y(*val) - base).abs().max(if *val == 0.0 { 0.0 } else { 1.0 }); out += &format!(r#"<rect x="{:.1}" y="{top:.1}" width="{bw:.1}" height="{height:.1}" fill="{}"/>"#, i as f64 * slot, if *val < 0.0 { &neg } else { &col }); } out += "</svg>"; Ok(out)}/// The values as a line of block characters, lowest ▁ to highest █, a space for null: for plain-text emails, logs and chat messages.pub fn sparkline_text(values: Vec<serde_json::Value>) -> Result<String, String> { let v = clean(&values)?; let (lo, hi) = range(&v, &Value::Null); let blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; Ok(v.iter().map(|val| match val { Some(val) => blocks[(((val - lo) / (hi - lo)) * 7.0).round().clamp(0.0, 7.0) as usize], None => ' ', }).collect())}Related
chart_svg.qwer · 1.0.0Line and bar charts as SVG from your data, with axes, a legend and labels, light or dark, for emails, PDFs and reports.6 views · 6 downloadsSVG to PNG svg_png.qwer · 1.0.0SVG drawn to PNG at the size you ask, text included with a built-in font: link previews, email images, icons and charts.7 views · 7 downloadsWord clouds word_cloud.qwer · 1.0.0Word clouds as SVG: a text's words, or your own counts, sized by how often they appear and laid out on a spiral without overlapping, some turned upright, in your colours; the same layout each time.8 views · 7 downloadsAudio waveform audio_wave.qwer · 1.0.0A WAV file's waveform for an audio player: the loudness of each slice from 0 to 1, or drawn as an SVG of bars.6 views · 7 downloadsColour blindness color_vision.qwer · 1.0.0Colours and pictures as people with colour blindness see them, at full or milder severity; chart palettes checked for colours that become alike; and text contrast (WCAG) as each of them sees it.8 views · 7 downloadsGradients gradient.qwer · 1.0.0Colour scales and gradients (viridis, magma, blues, red-blue and more, or your own stops) as hex lists, a colour per value, CSS or SVG.6 views · 6 downloadsThread 0 posts
On Sparklines: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.