This website is meant to be read and understood quickly by humans, but is only fully parsable, on a technical level, with the aid of an AI system. Read why →
Loop MMT
Numbers In, One Inline Sparkline Outrender← all gifts

Sparkline-sink

sparkline-sink turns a numeric series into one inline unicode sparkline with zero dependencies — a single line of eight block levels (▁▂▃▄▅▆▇█), one glyph per value. It renders no axis, labels, numbers, or color — only the shape — so it drops straight into a log line, a commit message, a terminal, or a table cell where a chart library would be absurd. The load-bearing property: because each value maps to an INTEGER bucket, no float ever reaches the output, so the same series in produces byte-identical glyphs out — in Node or a browser, committable and diffable.

The honest edge
sparkline-sink is a GLYPH sparkline, not a chart: one line of 8 discrete block levels, one glyph per value, no axis, labels, numbers, color, or scale markers. Two values in the same eighth of the range draw the SAME glyph — it shows TREND, not magnitude, and it is not a substitute for the number. It takes NUMBERS ONLY (no caller text reaches the output, so there is no escaping surface). A non-finite value is a hard error, never a guessed point. Same series in → byte-identical line out.
Run it
echo '[3,1,4,1,5,9,2,6]' | node sparkline-sink.js # ▃▁▄▁▅█▂▆ (--min N / --max N to pin the domain) test_sparkline-sink.js (12/12, out-of-band hand-computed glyph oracle) + Plumb conformance GREEN (I1-I5, signed, clock-independent) Node / browser, no dependencies
The code — every file that ships
sparkline-sink.js156 lineson GitHub →
#!/usr/bin/env node
/*
 * sparkline-sink — turn a numeric series into one inline unicode sparkline, no deps.
 * MIT · zero-dependency · standalone gift · lane: sink (consumes data, emits an artifact).
 *
 * THE PRINTED EDGE (read before trusting the output):
 *   This is a GLYPH sparkline, not a chart. It emits ONE line of unicode block
 *   characters (▁▂▃▄▅▆▇█), one glyph per value, mapping each value to one of
 *   EXACTLY 8 discrete levels across the data's [min,max] range. It renders NO
 *   axis, labels, numbers, color, or scale markers — only the shape. Because there
 *   are only 8 levels, two values in the same eighth of the range draw the SAME
 *   glyph: it shows TREND, not magnitude, and it is not a substitute for the number.
 *   It takes NUMBERS ONLY — no caller text ever reaches the output, so there is no
 *   escaping surface. A non-finite value (NaN / Infinity) is a hard error, never a
 *   silently-dropped or guessed point. Output is a PURE FUNCTION of the input: same
 *   series in → byte-identical line out.
 *
 * USAGE:
 *   echo '[3,1,4,1,5,9,2,6]'          | node sparkline-sink.js     # ▃▁▄▁▅█▂▆  (+ newline)
 *   echo '[0,1,2,3,4,5,6,7]'          | node sparkline-sink.js     # ▁▂▃▄▅▆▇█
 *   echo '{"series":[5],"min":0,"max":10}' | node sparkline-sink.js
 *   node sparkline-sink.js --help
 *
 * INPUT (stdin, JSON): either
 *   - a bare array of numbers                 -> [1,2,3]
 *   - a spec object { series, min, max }      -> pin the domain instead of auto-fit
 *   CLI flags (--min --max) OVERRIDE object fields.
 *
 * OUTPUT (stdout): one line of block glyphs (UTF-8), trailing newline. Empty series -> a bare newline.
 *
 * DETERMINISM: each value maps to an INTEGER level (a bucket), so no float noise or
 *   platform drift can reach the output. Same series → same bytes, in Node or a browser.
 * PORTABILITY: pure JS on a plain array — identical in Node and the browser.
 */
'use strict';

// The ONLY output alphabet: 8 block levels, low to high. U+2581 .. U+2588.
var LEVELS = ['\u2581', '\u2582', '\u2583', '\u2584', '\u2585', '\u2586', '\u2587', '\u2588'];

function finite(v, j) {
  if (typeof v !== 'number' || !isFinite(v)) {
    throw new Error('series[' + j + '] is not a finite number: ' + JSON.stringify(v));
  }
  return v;
}

// Shared shape with svg-sink's domain(): auto-fit [min(data),max(data)] unless pinned;
// a flat series opens to a unit window so every value lands mid-scale (never a divide-by-0).
function domain(series, min, max) {
  var lo = (min != null) ? Number(min) : Infinity;
  var hi = (max != null) ? Number(max) : -Infinity;
  if (min == null || max == null) {
    for (var j = 0; j < series.length; j++) {
      var v = series[j];
      if (min == null && v < lo) lo = v;
      if (max == null && v > hi) hi = v;
    }
  }
  if (!isFinite(lo)) lo = 0;
  if (!isFinite(hi)) hi = 0;
  if (lo === hi) { lo -= 1; hi += 1; }   // flat series -> unit window, drawn mid-scale
  return { lo: lo, hi: hi };
}

function level(v, lo, hi) {
  var k = Math.floor(((v - lo) / (hi - lo)) * LEVELS.length);
  if (k < 0) k = 0;
  if (k >= LEVELS.length) k = LEVELS.length - 1;   // v == hi -> 8 clamps to 7
  return k;
}

function normalize(input, flags) {
  var spec = {};
  var seriesRaw;
  if (input && !Array.isArray(input) && typeof input === 'object') {
    seriesRaw = input.series;
    if (input.min != null) spec.min = input.min;
    if (input.max != null) spec.max = input.max;
  } else {
    seriesRaw = input;
  }
  flags = flags || {};
  if (flags.min != null) spec.min = flags.min;       // CLI flags override object fields
  if (flags.max != null) spec.max = flags.max;

  if (!Array.isArray(seriesRaw)) {
    throw new Error('input has no numeric series (expected an array or a {series:...} object)');
  }
  if (seriesRaw.length > 0 && Array.isArray(seriesRaw[0])) {
    throw new Error('sparkline-sink takes ONE series (a flat array of numbers), not nested arrays');
  }
  spec.series = seriesRaw.map(function (v, j) { return finite(v, j); });
  if (spec.min != null) { spec.min = numOrThrow(spec.min, 'min'); }
  if (spec.max != null) { spec.max = numOrThrow(spec.max, 'max'); }
  if (spec.min != null && spec.max != null && spec.min >= spec.max) {
    throw new Error('min must be < max (got min=' + spec.min + ', max=' + spec.max + ')');
  }
  return spec;
}
function numOrThrow(v, name) {
  var n = Number(v);
  if (typeof n !== 'number' || !isFinite(n)) throw new Error(name + ' must be a finite number, got ' + JSON.stringify(v));
  return n;
}

// ---- the gift: series -> one line of block glyphs -------------------------------
function sparkline(input, flags) {
  var spec = normalize(input, flags);
  var d = domain(spec.series, spec.min, spec.max);
  var out = '';
  for (var j = 0; j < spec.series.length; j++) {
    out += LEVELS[level(spec.series[j], d.lo, d.hi)];
  }
  return out + '\n';
}

// ---- exports (Node require + browser attach) ------------------------------------
if (typeof module !== 'undefined' && module.exports) {
  module.exports = { sparkline: sparkline, normalize: normalize, domain: domain, LEVELS: LEVELS };
}
if (typeof window !== 'undefined') {
  window.LoopGifts = window.LoopGifts || {};
  window.LoopGifts['sparkline-sink'] = { sparkline: sparkline, LEVELS: LEVELS };
}

// ---- CLI ------------------------------------------------------------------------
function main() {
  var argv = process.argv.slice(2);
  if (argv.indexOf('--help') !== -1 || argv.indexOf('-h') !== -1) {
    process.stdout.write(
      'sparkline-sink — numbers -> one inline unicode sparkline (' + LEVELS.join('') + '), zero deps.\n\n' +
      'USAGE:\n' +
      "  echo '[3,1,4,1,5,9,2,6]' | node sparkline-sink.js [--min N] [--max N]\n\n" +
      'INPUT (stdin, JSON): a bare array of numbers, or { "series": [...], "min": N, "max": N }.\n' +
      'OUTPUT: one line of 8-level block glyphs, trailing newline. Numbers only; non-finite is a hard error.\n' +
      'EDGE: 8 discrete levels — shows shape, not magnitude; no axis/labels/color.\n');
    return;
  }
  var flags = {};
  for (var i = 0; i < argv.length; i++) {
    if (argv[i] === '--min') flags.min = Number(argv[++i]);
    else if (argv[i] === '--max') flags.max = Number(argv[++i]);
  }
  var chunks = [];
  process.stdin.on('data', function (c) { chunks.push(c); });
  process.stdin.on('end', function () {
    var raw = chunks.join('').trim();
    if (!raw) { process.stdout.write('\n'); return; }   // empty stdin -> empty sparkline
    var input;
    try { input = JSON.parse(raw); }
    catch (e) { process.stderr.write('sparkline-sink: input is not valid JSON: ' + e.message + '\n'); process.exit(1); }
    try { process.stdout.write(sparkline(input, flags)); }
    catch (e) { process.stderr.write('sparkline-sink: ' + e.message + '\n'); process.exit(1); }
  });
}
if (typeof require !== 'undefined' && require.main === module) { main(); }
test_sparkline-sink.js95 lineson GitHub →
#!/usr/bin/env node
/*
 * test_sparkline-sink.js — drift-check battery for the sparkline-sink gift.
 *
 * THE ORACLE IS OUT-OF-BAND BY CONSTRUCTION. The expected glyph lines below are
 * hand-computed from the documented level math — level(v) = clamp(floor((v-lo)/(hi-lo)*8), 0, 7),
 * lo/hi the auto-fit or pinned domain — written independently of the emitter, not captured
 * from its output. A build cannot certify itself, so the expected strings are the author's fact.
 * The remaining checks are contract properties (determinism, honest errors, alphabet-closed).
 *
 * Run: node test_sparkline-sink.js   (exit 0 = all pass; nonzero = a named failure)
 */
'use strict';
const assert = require('assert');
const { sparkline, LEVELS } = require('./sparkline-sink.js');
const [L0, L1, L2, L3, L4, L5, L6, L7] = LEVELS; // ▁▂▃▄▅▆▇█

let n = 0, passed = 0;
function check(name, fn) {
  n++;
  try { fn(); passed++; console.log('  ok   ' + name); }
  catch (e) { console.log('  FAIL ' + name + '  — ' + e.message); process.exitCode = 1; }
}

// 1 — the clean 8-step ramp: [0..7] auto-domain [0,7] -> one glyph per level, in order.
check('ramp: [0..7] -> ▁▂▃▄▅▆▇█ (every level, in order)', () => {
  assert.strictEqual(sparkline([0, 1, 2, 3, 4, 5, 6, 7]), '\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588\n');
});

// 2 — flat series: all equal -> unit window -> all mid-scale (level 4 = ▅), never a divide-by-0.
check('flat: [1,1,1] -> ▅▅▅ (mid-scale, no NaN)', () => {
  assert.strictEqual(sparkline([1, 1, 1]), L4 + L4 + L4 + '\n');
});

// 3 — single value: flat window -> one mid-scale glyph.
check('single: [5] -> ▅', () => {
  assert.strictEqual(sparkline([5]), L4 + '\n');
});

// 4 — two-point extremes: [0,10] auto [0,10] -> low, high (10 clamps 8->7).
check('extremes: [0,10] -> ▁█', () => {
  assert.strictEqual(sparkline([0, 10]), L0 + L7 + '\n');
});

// 5 — negatives across zero: [-4,0,4] auto [-4,4] -> ▁▅█.
check('negatives: [-4,0,4] -> ▁▅█', () => {
  assert.strictEqual(sparkline([-4, 0, 4]), L0 + L4 + L7 + '\n');
});

// 6 — the classic: [3,1,4,1,5,9,2,6] auto [1,9] (hi-lo=8 -> level=floor(v-1)).
//     3->2 4->3 1->0 5->4 9->8clamp7 2->1 6->5  => ▃▁▄▁▅█▂▆
check('classic: [3,1,4,1,5,9,2,6] -> ▃▁▄▁▅█▂▆', () => {
  assert.strictEqual(sparkline([3, 1, 4, 1, 5, 9, 2, 6]), L2 + L0 + L3 + L0 + L4 + L7 + L1 + L5 + '\n');
});

// 7 — pinned domain: [5] with min0 max10 -> ▅ (level 4), overriding the flat-window auto-fit.
check('pinned: {series:[5],min:0,max:10} -> ▅', () => {
  assert.strictEqual(sparkline({ series: [5], min: 0, max: 10 }), L4 + '\n');
  // CLI flags override object fields:
  assert.strictEqual(sparkline({ series: [5], min: 0, max: 10 }, { min: 0, max: 2 }), L7 + '\n'); // 5 > max2 -> clamp high
});

// 8 — empty series -> a bare newline, never a crash.
check('empty: [] -> "\\n"', () => {
  assert.strictEqual(sparkline([]), '\n');
});

// 9 — determinism: same series -> byte-identical output, twice.
check('determinism: repeated render is byte-identical', () => {
  const s = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
  assert.strictEqual(sparkline(s), sparkline(s));
});

// 10 — alphabet is CLOSED: every output char is one of the 8 block levels or the trailing \n.
check('closed alphabet: only ▁▂▃▄▅▆▇█ (+ newline) ever appear', () => {
  const out = sparkline([3, -2, 7, 0, 5, -4, 9, 1, 8]);
  const body = out.replace(/\n$/, '');
  for (const ch of body) assert.ok(LEVELS.indexOf(ch) !== -1, 'unexpected char ' + JSON.stringify(ch));
  assert.strictEqual(body.length, 9, 'one glyph per value');
});

// 11 — non-finite is a hard, named error (never a guessed point).
check('non-finite value throws, naming the position', () => {
  assert.throws(() => sparkline([1, NaN, 3]), /series\[1\] is not a finite number/);
  assert.throws(() => sparkline([1, 2, Infinity]), /series\[2\]/);
});

// 12 — honest input errors: nested arrays and bad min/max throw rather than mis-render.
check('nested arrays and inverted domain throw', () => {
  assert.throws(() => sparkline([[1, 2], [3, 4]]), /ONE series/);
  assert.throws(() => sparkline({ series: [1, 2], min: 5, max: 5 }), /min must be < max/);
});

console.log('\n' + passed + '/' + n + ' passed');
if (passed !== n) process.exit(1);
Take the whole folder → MIT Node / browser, no dependencies