import { Sketch } from './draw.jsx'; import { ink, rgb } from './mrly.js'; import { board, line, axis, tag } from './chart.js'; const HEAD = 15; const CHAR = 6.7; // NUMBERS export function mix(from, to, at) { const [a, b] = [rgb(from), rgb(to)]; const f = Math.max(0, Math.min(1, at)); return `rgb(${a.map((c, k) => Math.round(c + (b[k] - c) * f)).join(',')})`; } function text(t) { return String(t ?? '').trim(); } function bare(t) { const s = text(t); return (s.startsWith('-') ? s.slice(1) : s).replace(/^0+(?=\d)/, ''); } function negative(t) { return text(t).startsWith('-'); } function log10(t) { const d = bare(t); if (/^\d+$/.test(d)) return d === '0' ? -Infinity : Math.log10(Number(d.slice(0, HEAD))) + Math.max(0, d.length - HEAD); const v = Math.abs(Number(t)); return v > 0 ? Math.log10(v) : -Infinity; } function span(values) { const tops = values.map(log10).filter(Number.isFinite); return { hi: tops.length ? Math.max(...tops) : 0, lo: tops.length ? Math.min(...tops) : 0 }; } function share(t, hi) { const l = log10(t); return l === -Infinity ? 0 : Math.min(1, 10 ** (l - hi)); } function widest(values) { return values.reduce((w, v) => Math.max(w, text(v).length), 0); } function ticks(n, start) { const every = Math.max(1, Math.ceil(n / 12)); const out = []; for (let i = 0; i < n; i++) if (i % every === 0) out.push([(i + 0.5) / n, String(start + i)]); return out; } function running(terms) { try { let total = 0n; return terms.map((t) => (total += BigInt(text(t))).toString()); } catch { return null; } } function expand(t, base) { const d = bare(t); if (!/^\d+$/.test(d)) return []; const digits = base === 10 ? d : BigInt(d).toString(base); return [...digits].map((c) => parseInt(c, 36)); } // PINS export function Pins({ terms, start = 0, log = 'auto', height = 240, label = '', note, marks, hue = ink.blue, className = 'bars', style, ...rest }) { const draw = (canvas) => { const n = terms.length; const room = n > 0 && (canvas.clientWidth - 28) / n >= widest(terms) * CHAR + 10; const b = board(canvas, height, { top: room ? 36 : 26 }); if (!n) return; const { hi, lo } = span(terms); const scaled = log === 'auto' ? hi - lo >= 2 : !!log; const at = (t) => { const l = log10(t); if (!scaled) return share(t, hi); return l === -Infinity ? 0 : (l - lo + 1) / (hi - lo + 1); }; const step = b.wide / n; terms.forEach((t, i) => { const f = (i + 0.5) / n; const y = Math.max(0.015, at(t)); const color = marks?.[i] ? ink.yellow : negative(t) ? ink.orange : hue; line(b, [[f, 0], [f, y]], color, { width: Math.max(1, Math.min(3, step * 0.16)) }); line(b, [[f, y]], color, { dots: Math.max(2, Math.min(4, step * 0.14)) }); if (room) tag(b, text(t), ink.fg, 'center', b.x(f), Math.max(b.roof - 8, b.y(y) - 8)); }); axis(b, ticks(n, start)); tag(b, `${scaled ? 'log scale' : 'linear scale'}${label ? `, ${label}` : ''}`, ink.dim); tag(b, note ?? `last ${text(terms.at(-1))}`, ink.fg, 'right'); }; return ; } // STAIRCASE export function Staircase({ terms, start = 0, sums = false, height = 220, label = '', hue = ink.green, className = 'bars', style, ...rest }) { const draw = (canvas) => { const b = board(canvas, height); const values = (sums && running(terms)) || terms; const n = values.length; if (!n) return; const { hi } = span(values); const points = []; values.forEach((v, i) => { const f = share(v, hi); points.push([i / n, f], [(i + 1) / n, f]); }); line(b, points, hue, { width: 1.6, fill: 0.16 }); axis(b, ticks(n, start)); const kind = values === terms ? 'step function' : 'partial sums'; tag(b, `${kind}${values.some(negative) ? ', magnitudes' : ''}${label ? `, ${label}` : ''}`, ink.dim); }; return ; } // DIGITS export function Digits({ terms, base = 2, start = 0, cell = 13, label = '', className = 'bars', style, ...rest }) { const draw = (canvas) => { const q = Math.max(2, Math.min(36, Math.round(base))); const rows = terms.map((t) => expand(t, q)); const cols = Math.max(1, rows.reduce((w, r) => Math.max(w, r.length), 0)); const gutter = 42; const free = Math.max(40, canvas.clientWidth - gutter - 10); const size = Math.max(2, Math.min(cell, free / cols)); const b = board(canvas, Math.round(40 + rows.length * size), { left: gutter, right: 10, top: 30, bottom: 10 }); rows.forEach((digits, i) => { const y = b.roof + i * size; if (size >= 8) tag(b, String(start + i), ink.dim, 'right', gutter - 8, y + size - 3); digits.forEach((d, j) => { b.ctx.fillStyle = d === 0 ? ink.line : mix(ink.blue, ink.yellow, q > 2 ? d / (q - 1) : 1); b.ctx.fillRect(gutter + (cols - digits.length + j) * size, y, Math.max(1, size - 1), Math.max(1, size - 1)); }); }); tag(b, `${label ? `${label}, ` : ''}base ${q}, least significant at the right`, ink.dim); }; return ; } // RATIOS export function Ratios({ values, start = 0, height = 96, label = '', className = 'bars', style, ...rest }) { const draw = (canvas) => { const b = board(canvas, height, { top: 26, bottom: 20 }); const n = values.length; if (!n) return; const nums = values.map(Number).filter(Number.isFinite); const lo = Math.min(...nums), hi = Math.max(...nums); const step = b.wide / n; const room = step >= widest(values) * CHAR + 8; values.forEach((v, i) => { const at = hi > lo ? (Number(v) - lo) / (hi - lo) : 0.5; b.ctx.fillStyle = mix(ink.blue, ink.pink, Number.isFinite(at) ? at : 0.5); b.ctx.fillRect(b.x(i / n), b.roof, Math.max(1, step - 1), b.floor - b.roof); if (room) tag(b, text(v), ink.deep, 'center', b.x((i + 0.5) / n), (b.roof + b.floor) / 2 + 4); }); axis(b, ticks(n, start)); tag(b, label || 'each term against the one before', ink.dim); }; return ; } // DIFFERENCES export function Differences({ rows, cell = 26, label = '', className = 'bars', style, ...rest }) { const draw = (canvas) => { const cols = Math.max(1, rows[0]?.length ?? 0); const gutter = 42; const free = Math.max(40, canvas.clientWidth - gutter - 10); const size = Math.max(6, Math.min(cell, free / cols)); const b = board(canvas, Math.round(40 + rows.length * size), { left: gutter, right: 10, top: 30, bottom: 10 }); rows.forEach((row, r) => { const { hi } = span(row); const y = b.roof + r * size; if (size >= 10) tag(b, r ? `d${r}` : 'terms', ink.dim, 'right', gutter - 8, y + size - 6); row.forEach((v, i) => { const x = gutter + (i + r / 2) * size; b.ctx.globalAlpha = 0.25 + 0.7 * share(v, hi); b.ctx.fillStyle = log10(v) === -Infinity ? ink.dim : negative(v) ? ink.orange : ink.blue; b.ctx.fillRect(x, y, Math.max(1, size - 1), Math.max(1, size - 1)); b.ctx.globalAlpha = 1; if (size >= text(v).length * CHAR + 4) tag(b, text(v), ink.deep, 'center', x + size / 2, y + size - 7); }); }); tag(b, label || 'the difference triangle, each row the differences of the row above', ink.dim); }; return ; } // TERMS export function Terms({ terms, start = 0, marks, capped, tight, empty = '', onPick, label }) { if (!terms.length && empty) return
{empty}
; return (
{label ? {label} : null} {terms.map((t, i) => ( onPick(t, start + i) : undefined}> {tight ? null : {start + i}} {text(t)} ))} {capped ? to the budget : null}
); }