series.jsx

8.3 kB · jsx · 216 lines

1import { Sketch } from './draw.jsx';2import { ink, rgb } from './mrly.js';3import { board, line, axis, tag } from './chart.js';45const HEAD = 15;6const CHAR = 6.7;78// NUMBERS910export function mix(from, to, at) {11  const [a, b] = [rgb(from), rgb(to)];12  const f = Math.max(0, Math.min(1, at));13  return `rgb(${a.map((c, k) => Math.round(c + (b[k] - c) * f)).join(',')})`;14}1516function text(t) {17  return String(t ?? '').trim();18}1920function bare(t) {21  const s = text(t);22  return (s.startsWith('-') ? s.slice(1) : s).replace(/^0+(?=\d)/, '');23}2425function negative(t) {26  return text(t).startsWith('-');27}2829function log10(t) {30  const d = bare(t);31  if (/^\d+$/.test(d)) return d === '0' ? -Infinity : Math.log10(Number(d.slice(0, HEAD))) + Math.max(0, d.length - HEAD);32  const v = Math.abs(Number(t));33  return v > 0 ? Math.log10(v) : -Infinity;34}3536function span(values) {37  const tops = values.map(log10).filter(Number.isFinite);38  return { hi: tops.length ? Math.max(...tops) : 0, lo: tops.length ? Math.min(...tops) : 0 };39}4041function share(t, hi) {42  const l = log10(t);43  return l === -Infinity ? 0 : Math.min(1, 10 ** (l - hi));44}4546function widest(values) {47  return values.reduce((w, v) => Math.max(w, text(v).length), 0);48}4950function ticks(n, start) {51  const every = Math.max(1, Math.ceil(n / 12));52  const out = [];53  for (let i = 0; i < n; i++) if (i % every === 0) out.push([(i + 0.5) / n, String(start + i)]);54  return out;55}5657function running(terms) {58  try {59    let total = 0n;60    return terms.map((t) => (total += BigInt(text(t))).toString());61  } catch {62    return null;63  }64}6566function expand(t, base) {67  const d = bare(t);68  if (!/^\d+$/.test(d)) return [];69  const digits = base === 10 ? d : BigInt(d).toString(base);70  return [...digits].map((c) => parseInt(c, 36));71}7273// PINS7475export function Pins({ terms, start = 0, log = 'auto', height = 240, label = '', note, marks, hue = ink.blue, className = 'bars', style, ...rest }) {76  const draw = (canvas) => {77    const n = terms.length;78    const room = n > 0 && (canvas.clientWidth - 28) / n >= widest(terms) * CHAR + 10;79    const b = board(canvas, height, { top: room ? 36 : 26 });80    if (!n) return;81    const { hi, lo } = span(terms);82    const scaled = log === 'auto' ? hi - lo >= 2 : !!log;83    const at = (t) => {84      const l = log10(t);85      if (!scaled) return share(t, hi);86      return l === -Infinity ? 0 : (l - lo + 1) / (hi - lo + 1);87    };88    const step = b.wide / n;89    terms.forEach((t, i) => {90      const f = (i + 0.5) / n;91      const y = Math.max(0.015, at(t));92      const color = marks?.[i] ? ink.yellow : negative(t) ? ink.orange : hue;93      line(b, [[f, 0], [f, y]], color, { width: Math.max(1, Math.min(3, step * 0.16)) });94      line(b, [[f, y]], color, { dots: Math.max(2, Math.min(4, step * 0.14)) });95      if (room) tag(b, text(t), ink.fg, 'center', b.x(f), Math.max(b.roof - 8, b.y(y) - 8));96    });97    axis(b, ticks(n, start));98    tag(b, `${scaled ? 'log scale' : 'linear scale'}${label ? `, ${label}` : ''}`, ink.dim);99    tag(b, note ?? `last ${text(terms.at(-1))}`, ink.fg, 'right');100  };101  return <Sketch className={className} style={{ height, ...style }} draw={draw} deps={[terms, log, start, marks, height]} {...rest} />;102}103104// STAIRCASE105106export function Staircase({ terms, start = 0, sums = false, height = 220, label = '', hue = ink.green, className = 'bars', style, ...rest }) {107  const draw = (canvas) => {108    const b = board(canvas, height);109    const values = (sums && running(terms)) || terms;110    const n = values.length;111    if (!n) return;112    const { hi } = span(values);113    const points = [];114    values.forEach((v, i) => {115      const f = share(v, hi);116      points.push([i / n, f], [(i + 1) / n, f]);117    });118    line(b, points, hue, { width: 1.6, fill: 0.16 });119    axis(b, ticks(n, start));120    const kind = values === terms ? 'step function' : 'partial sums';121    tag(b, `${kind}${values.some(negative) ? ', magnitudes' : ''}${label ? `, ${label}` : ''}`, ink.dim);122  };123  return <Sketch className={className} style={{ height, ...style }} draw={draw} deps={[terms, sums, start, height]} {...rest} />;124}125126// DIGITS127128export function Digits({ terms, base = 2, start = 0, cell = 13, label = '', className = 'bars', style, ...rest }) {129  const draw = (canvas) => {130    const q = Math.max(2, Math.min(36, Math.round(base)));131    const rows = terms.map((t) => expand(t, q));132    const cols = Math.max(1, rows.reduce((w, r) => Math.max(w, r.length), 0));133    const gutter = 42;134    const free = Math.max(40, canvas.clientWidth - gutter - 10);135    const size = Math.max(2, Math.min(cell, free / cols));136    const b = board(canvas, Math.round(40 + rows.length * size), { left: gutter, right: 10, top: 30, bottom: 10 });137    rows.forEach((digits, i) => {138      const y = b.roof + i * size;139      if (size >= 8) tag(b, String(start + i), ink.dim, 'right', gutter - 8, y + size - 3);140      digits.forEach((d, j) => {141        b.ctx.fillStyle = d === 0 ? ink.line : mix(ink.blue, ink.yellow, q > 2 ? d / (q - 1) : 1);142        b.ctx.fillRect(gutter + (cols - digits.length + j) * size, y, Math.max(1, size - 1), Math.max(1, size - 1));143      });144    });145    tag(b, `${label ? `${label}, ` : ''}base ${q}, least significant at the right`, ink.dim);146  };147  return <Sketch className={className} draw={draw} deps={[terms, base, start, cell]} style={style} {...rest} />;148}149150// RATIOS151152export function Ratios({ values, start = 0, height = 96, label = '', className = 'bars', style, ...rest }) {153  const draw = (canvas) => {154    const b = board(canvas, height, { top: 26, bottom: 20 });155    const n = values.length;156    if (!n) return;157    const nums = values.map(Number).filter(Number.isFinite);158    const lo = Math.min(...nums), hi = Math.max(...nums);159    const step = b.wide / n;160    const room = step >= widest(values) * CHAR + 8;161    values.forEach((v, i) => {162      const at = hi > lo ? (Number(v) - lo) / (hi - lo) : 0.5;163      b.ctx.fillStyle = mix(ink.blue, ink.pink, Number.isFinite(at) ? at : 0.5);164      b.ctx.fillRect(b.x(i / n), b.roof, Math.max(1, step - 1), b.floor - b.roof);165      if (room) tag(b, text(v), ink.deep, 'center', b.x((i + 0.5) / n), (b.roof + b.floor) / 2 + 4);166    });167    axis(b, ticks(n, start));168    tag(b, label || 'each term against the one before', ink.dim);169  };170  return <Sketch className={className} style={{ height, ...style }} draw={draw} deps={[values, start, height]} {...rest} />;171}172173// DIFFERENCES174175export function Differences({ rows, cell = 26, label = '', className = 'bars', style, ...rest }) {176  const draw = (canvas) => {177    const cols = Math.max(1, rows[0]?.length ?? 0);178    const gutter = 42;179    const free = Math.max(40, canvas.clientWidth - gutter - 10);180    const size = Math.max(6, Math.min(cell, free / cols));181    const b = board(canvas, Math.round(40 + rows.length * size), { left: gutter, right: 10, top: 30, bottom: 10 });182    rows.forEach((row, r) => {183      const { hi } = span(row);184      const y = b.roof + r * size;185      if (size >= 10) tag(b, r ? `d${r}` : 'terms', ink.dim, 'right', gutter - 8, y + size - 6);186      row.forEach((v, i) => {187        const x = gutter + (i + r / 2) * size;188        b.ctx.globalAlpha = 0.25 + 0.7 * share(v, hi);189        b.ctx.fillStyle = log10(v) === -Infinity ? ink.dim : negative(v) ? ink.orange : ink.blue;190        b.ctx.fillRect(x, y, Math.max(1, size - 1), Math.max(1, size - 1));191        b.ctx.globalAlpha = 1;192        if (size >= text(v).length * CHAR + 4) tag(b, text(v), ink.deep, 'center', x + size / 2, y + size - 7);193      });194    });195    tag(b, label || 'the difference triangle, each row the differences of the row above', ink.dim);196  };197  return <Sketch className={className} draw={draw} deps={[rows, cell]} style={style} {...rest} />;198}199200// TERMS201202export function Terms({ terms, start = 0, marks, capped, tight, empty = '', onPick, label }) {203  if (!terms.length && empty) return <div className="ribbon"><span className="cap">{empty}</span></div>;204  return (205    <div className={tight ? 'ribbon tight' : 'ribbon'}>206      {label ? <span className="tag">{label}</span> : null}207      {terms.map((t, i) => (208        <span key={i} className={marks?.[i] ? 'yellow' : undefined} role={onPick ? 'button' : undefined} onClick={onPick ? () => onPick(t, start + i) : undefined}>209          {tight ? null : <i>{start + i}</i>}210          <b>{text(t)}</b>211        </span>212      ))}213      {capped ? <span className="cap">to the budget</span> : null}214    </div>215  );216}