Samples pouring into a histogram, the bars rising and settling into a bell curve with live stats.
response size (kB) n 792 mean 5.13 ──────────────────────────────────────────────────────────────────────────── 130 │┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈╎┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ │ • •• │ ▃▃ │ ██ 98 │┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈██ ██┈▁▁ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ │ ██ ██ ██ │ ▁▁ ██ ██ ██ │ ██ ██ ██ ██ ▆▆ 65 │┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈▄▄ ██┈██ ██┈██ ██┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ │ ██ ██ ██ ██ ██ ██ │ ▁▁ ██ ██ ██ ██ ██ ██ │ ██ ██ ██ ██ ██ ██ ██ │ ██ ██ ██ ██ ██ ██ ██ ██ 33 │┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ██┈██ ██┈██ ██┈██ ██┈██ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ ┈ │ ▆▆ ██ ██ ██ ██ ██ ██ ██ ██ ▁▁ │ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ▃▃ │ ▆▆ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ▁▁ 0 ┤┈ ┈ ┈ ┈ ┈▄▄ ██┈██ ██┈██ ██┈██ ██┈██ ██┈██ ██┈██ ██┈▆▆ ▃▃┈▁▁ ▁▁┈ ┈ └─────────────────────────────────────────────────────────────────── 0.7 2.1 3.5 4.8 6.2 7.5 8.9 10.3
Paste two lines into any page, play it in a terminal, or put a GIF in a README. The code follows the look you pick above.
<script type="module" src="https://www.ascii-magic.com/ascii/ascii.js"></script>
<ascii-art piece="histogram"></ascii-art>// copy histogram.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./histogram.js";
export function Piece() {
const ref = useRef(null);
useEffect(() => {
const draw = create(), t0 = performance.now();
let raf;
const tick = () => {
const f = draw((performance.now() - t0) / 1000);
ref.current.textContent = f.text ?? f;
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
return <pre ref={ref} style={{ lineHeight: 1, fontFamily: "Menlo, Consolas, monospace" }} />;
}npx --yes https://www.ascii-magic.com/ascii/cli.tgz histogram<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/histogram.gif" alt="Histogram, animated ASCII art" width="720">
</p>Pause on the frame you like, pick a look, then download it at an exact size.
MIT licensed. It imports one small helper file, kit.js.
/* histogram: samples pouring into a distribution histogram, bars rising and settling into a bell. */
import { Grid, palette, rng, clamp, gradient } from "../lib/kit.js";
const BAR = gradient(["#1f6feb", "#79c0ff", "#d2a8ff", "#f778ba"], 10); // 0-9
export const meta = {
slug: "histogram",
name: "Histogram",
category: "live-data",
note: "Samples pouring into a histogram, the bars rising and settling into a bell curve with live stats.",
tags: ["histogram", "distribution", "statistics", "chart", "dashboard"],
cols: 76, rows: 24, fps: 24,
palette: palette([...BAR, "#e6edf3", "#8b949e", "#30363d", "#ffa657", "#7ee787"]),
};
const BINS = 22, TOTAL = 2400;
const EIGHTH = " ▁▂▃▄▅▆▇█";
export default function create() {
const { cols, rows } = meta;
const g = new Grid(cols, rows);
const r = rng(5);
const samples = [];
for (let i = 0; i < TOTAL; i++) {
const u = Math.max(1e-9, r()), v = r();
const z = Math.sqrt(-2 * Math.log(u)) * Math.cos(6.2831853 * v);
samples.push(5 + z * 1.1 + (r() < 0.12 ? 1.6 : 0));
}
const LO = 0.5, HI = 10.5;
const binOf = (v) => Math.floor(((v - LO) / (HI - LO)) * BINS);
// cumulative counts per bin for each sample index, every 20 samples
const STEP = 20, snaps = [];
const cur = new Array(BINS).fill(0);
for (let i = 0; i <= TOTAL; i++) {
if (i % STEP === 0) snaps.push(cur.slice());
if (i < TOTAL) { const b = binOf(samples[i]); if (b >= 0 && b < BINS) cur[b]++; }
}
const final = snaps[snaps.length - 1];
const FMAX = Math.max(...final);
const X0 = 7, BW = 2, GAP = 1, Y1 = rows - 4, PH = Y1 - 3;
return (t) => {
g.clear();
const P = 10, lt = t % P;
const prog = clamp(lt / 6.5);
const eased = prog * prog * (3 - 2 * prog);
const fade = clamp((lt - 9.3) / 0.7);
const n = Math.round(eased * TOTAL);
const snap = snaps[Math.floor(n / STEP)];
const scaleMax = Math.max(FMAX * 0.35, Math.max(...snap)) * 1.05;
// axes
for (let k = 0; k <= 4; k++) {
const y = Y1 - Math.round((k / 4) * PH);
g.text(0, y, String(Math.round((k / 4) * scaleMax)).padStart(4), "b");
for (let x = X0; x < X0 + BINS * (BW + GAP); x += 2) g.set(x, y, "┈", "c");
g.set(X0 - 1, y, "┤", "b");
}
g.vline(X0 - 1, 3, Y1 - 1, "│", "b");
g.hline(X0 - 1, X0 + BINS * (BW + GAP), Y1 + 1, "─", "b");
g.set(X0 - 1, Y1 + 1, "└", "b");
let sum = 0, cnt = 0;
for (let b = 0; b < BINS; b++) {
const h = (snap[b] / scaleMax) * PH * (1 - fade);
const full = Math.floor(h), part = Math.floor((h - full) * 8);
const x = X0 + b * (BW + GAP);
const co = String(Math.min(9, Math.floor(Math.abs(b - BINS / 2) / (BINS / 2) * 10)));
for (let k = 0; k < full; k++) for (let i = 0; i < BW; i++) g.set(x + i, Y1 - k, "█", co);
if (part) for (let i = 0; i < BW; i++) g.set(x + i, Y1 - full, EIGHTH[part], co);
if (b % 3 === 0) g.text(x, Y1 + 2, (LO + ((b + 0.5) / BINS) * (HI - LO)).toFixed(1), "b");
const mid = LO + ((b + 0.5) / BINS) * (HI - LO);
sum += mid * snap[b]; cnt += snap[b];
}
// the falling sample
if (prog < 1 && n < TOTAL) {
for (let k = 0; k < 3; k++) {
const s = samples[Math.min(TOTAL - 1, n + k * 7)];
const b = binOf(s);
if (b >= 0 && b < BINS) g.set(X0 + b * (BW + GAP) + (k & 1), 2 + ((Math.floor(t * 20) + k * 3) % 3), "•", "d");
}
}
const mean = cnt ? sum / cnt : 0;
const mx = X0 + Math.round(((mean - LO) / (HI - LO)) * BINS * (BW + GAP));
if (cnt) for (let y = 3; y <= Y1; y += 2) if (g.get(mx, y) === " " || g.get(mx, y) === "┈") g.set(mx, y, "╎", "d");
g.text(1, 0, "response size", "a");
g.text(15, 0, "(kB)", "b");
const sx = cols - 22;
g.text(sx, 0, "n", "b"); g.text(sx + 2, 0, String(Math.round(n * (1 - fade))).padStart(5), "a");
g.text(sx + 9, 0, "mean", "b"); g.text(sx + 14, 0, mean.toFixed(2), "d");
g.hline(0, cols - 1, 1, "─", "c");
return g.frame();
};
}
/YOUR OWN
The pieces here are hand-built animations. To turn your own photo, logo or video into animated ASCII, use the ASCII Magic editor.