Skip to content

Entropy Visualizer — TypeScript source

Visualize the randomness quality of any data. See Shannon entropy, byte frequency distribution, chi-squared score, and a visual entropy heatmap.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

/**
 * Entropy Visualizer — pure byte-level randomness analysis. No DOM, no deps.
 *
 * Everything is deterministic: the same bytes always produce the same numbers.
 * (This file is the contract the Go CLI twin mirrors — see CLAUDE.md
 * "Dual source". Test vectors live in entropy-visualizer.test.ts.)
 */

export type EntropyVerdict = 'excellent' | 'good' | 'suspicious' | 'low';

export interface EntropyAnalysis {
  /** Shannon entropy in bits per byte (0 = one repeating byte, 8 = perfectly uniform). */
  shannonEntropy: number;
  /** χ² statistic against the uniform 256-bin expectation. */
  chiSquared: number;
  /** Upper-tail p-value for χ² with 255 degrees of freedom (1 = perfectly plausible). */
  chiSquaredPValue: number;
  /** Circular serial correlation between consecutive bytes (-1 … +1, 0 = uncorrelated). */
  serialCorrelation: number;
  /** Monte Carlo π estimate from consecutive byte pairs (≈3.14159 for random data). */
  monteCarloPi: number;
  /** Occurrence count per byte value 0-255 (always 256 entries). */
  byteFrequencies: number[];
  /** Shannon entropy (bits/byte) of each BLOCK_SIZE-byte block, for the heatmap. */
  blockEntropies: number[];
  verdict: EntropyVerdict;
}

/** Bytes per heatmap block. */
export const BLOCK_SIZE = 16;

/** Degrees of freedom for the byte-frequency χ² test (256 bins - 1 constraint). */
export const CHI_SQUARED_DF = 255;

/** Count occurrences of each byte value 0-255. */
export function countBytes(data: Uint8Array): number[] {
  const freq = new Array<number>(256).fill(0);
  for (let i = 0; i < data.length; i++) freq[data[i]]++;
  return freq;
}

/**
 * Shannon entropy H = -Σ p(x)·log2(p(x)) in bits per byte, computed from a
 * frequency histogram. Zero-count bins contribute nothing. `total` must be the
 * sum of `frequencies`.
 */
export function shannonBitsPerByte(frequencies: number[], total: number): number {
  if (total <= 0) return 0;
  let h = 0;
  for (const count of frequencies) {
    if (count === 0) continue;
    const p = count / total;
    h -= p * Math.log2(p);
  }
  return h;
}

/**
 * Pearson χ² comparing observed byte counts against a uniform expectation
 * E = total/256 per bin.
 */
export function chiSquaredStatistic(frequencies: number[], total: number): number {
  if (total <= 0) throw new Error('chi-squared needs a positive sample size');
  const expected = total / 256;
  let chi2 = 0;
  for (const observed of frequencies) {
    const diff = observed - expected;
    chi2 += (diff * diff) / expected;
  }
  return chi2;
}

/** Lanczos approximation (g=7, 9 coefficients) of ln Γ(x). */
function lnGamma(x: number): number {
  const g = [
    0.99999999999980993, 676.5203681218851, -1259.1392167224028,
    771.32342877765313, -176.61502916214059, 12.507343278686905,
    -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7,
  ];
  if (x < 0.5) {
    // Reflection formula: Γ(x)·Γ(1-x) = π / sin(πx)
    return Math.log(Math.PI / Math.sin(Math.PI * x)) - lnGamma(1 - x);
  }
  x -= 1;
  let a = g[0];
  const t = x + 7.5;
  for (let i = 1; i < 9; i++) a += g[i] / (x + i);
  return 0.5 * Math.log(2 * Math.PI) + (x + 0.5) * Math.log(t) - t + Math.log(a);
}

/**
 * Regularized upper incomplete gamma function Q(a, x) = Γ(a,x)/Γ(a), via the
 * power series (x < a+1) or the Lentz continued fraction (otherwise).
 * Numerical Recipes §6.2.
 */
export function gammaQ(a: number, x: number): number {
  if (a <= 0 || x < 0) return NaN;
  if (x === 0) return 1;
  if (x < a + 1) {
    // Series for P(a,x); Q = 1 - P
    let ap = a;
    let sum = 1 / a;
    let del = sum;
    for (let n = 0; n < 1000; n++) {
      ap += 1;
      del *= x / ap;
      sum += del;
      if (Math.abs(del) < Math.abs(sum) * 1e-15) break;
    }
    return Math.min(1, Math.max(0, 1 - sum * Math.exp(-x + a * Math.log(x) - lnGamma(a))));
  }
  // Continued fraction for Q(a,x)
  const FPMIN = 1e-300;
  let b = x + 1 - a;
  let c = 1 / FPMIN;
  let d = 1 / b;
  let h = d;
  for (let i = 1; i <= 1000; i++) {
    const an = -i * (i - a);
    b += 2;
    d = an * d + b;
    if (Math.abs(d) < FPMIN) d = FPMIN;
    c = b + an / c;
    if (Math.abs(c) < FPMIN) c = FPMIN;
    d = 1 / d;
    const del = d * c;
    h *= del;
    if (Math.abs(del - 1) < 1e-15) break;
  }
  return Math.min(1, Math.max(0, Math.exp(-x + a * Math.log(x) - lnGamma(a)) * h));
}

/** Upper-tail p-value for a χ² statistic with `df` degrees of freedom. */
export function chiSquaredP(chi2: number, df: number = CHI_SQUARED_DF): number {
  if (chi2 < 0 || df <= 0) return NaN;
  return gammaQ(df / 2, chi2 / 2);
}

/**
 * Circular serial correlation between consecutive bytes (the `ent` tool's
 * metric): scc = (Σxy - (Σx)²/n) / (Σx² - (Σx)²/n) over the pair sequence
 * (x₀,x₁), (x₁,x₂), …, (xₙ₋₁,x₀). 0 = uncorrelated, ±1 = perfectly
 * (anti)correlated. Constant input has a zero denominator → reported as 0
 * (nothing to correlate); inputs shorter than 2 bytes are also 0.
 */
export function serialCorrelationCoefficient(data: Uint8Array): number {
  const n = data.length;
  if (n < 2) return 0;
  let sum = 0;
  let sumSq = 0;
  let sumXY = 0;
  for (let i = 0; i < n; i++) {
    const x = data[i];
    const y = data[(i + 1) % n];
    sum += x;
    sumSq += x * x;
    sumXY += x * y;
  }
  const meanSq = (sum * sum) / n;
  const denom = sumSq - meanSq;
  if (denom === 0) return 0;
  return (sumXY - meanSq) / denom;
}

/**
 * Monte Carlo π estimate: consecutive byte pairs are (x, y) points in a
 * 256×256 square; the fraction inside the inscribed circle (center 127.5,
 * radius 128) times 4 estimates π. Inputs with fewer than 2 bytes → 0.
 */
export function monteCarloPiEstimate(data: Uint8Array): number {
  const pairs = Math.floor(data.length / 2);
  if (pairs === 0) return 0;
  let inside = 0;
  for (let i = 0; i < pairs; i++) {
    const dx = data[2 * i] - 127.5;
    const dy = data[2 * i + 1] - 127.5;
    if (dx * dx + dy * dy <= 128 * 128) inside++;
  }
  return (4 * inside) / pairs;
}

/** Shannon entropy (bits/byte) of each consecutive `blockSize`-byte block. */
export function blockEntropies(data: Uint8Array, blockSize: number = BLOCK_SIZE): number[] {
  if (blockSize < 1) throw new Error('blockSize must be at least 1');
  const blocks: number[] = [];
  for (let off = 0; off < data.length; off += blockSize) {
    const counts = new Array<number>(256).fill(0);
    let n = 0;
    const end = Math.min(off + blockSize, data.length);
    for (let i = off; i < end; i++) {
      counts[data[i]]++;
      n++;
    }
    blocks.push(shannonBitsPerByte(counts, n));
  }
  return blocks;
}

/** Map a Shannon entropy (bits/byte) to the verdict scale. */
export function verdictFromShannon(bitsPerByte: number): EntropyVerdict {
  if (bitsPerByte > 7.5) return 'excellent';
  if (bitsPerByte > 6.0) return 'good';
  if (bitsPerByte > 4.0) return 'suspicious';
  return 'low';
}

/**
 * Full analysis of a byte sequence. Throws on empty input — there is nothing
 * to measure and every metric would be undefined.
 */
export function analyzeEntropy(data: Uint8Array): EntropyAnalysis {
  if (data.length === 0) throw new Error('Nothing to analyze - provide at least 1 byte of data.');
  const total = data.length;
  const freq = countBytes(data);
  const shannon = shannonBitsPerByte(freq, total);
  const chi2 = chiSquaredStatistic(freq, total);
  return {
    shannonEntropy: shannon,
    chiSquared: chi2,
    chiSquaredPValue: chiSquaredP(chi2),
    serialCorrelation: serialCorrelationCoefficient(data),
    monteCarloPi: monteCarloPiEstimate(data),
    byteFrequencies: freq,
    blockEntropies: blockEntropies(data),
    verdict: verdictFromShannon(shannon),
  };
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →