// Which points of a curve does each dot-column of a graph draw? // // A curve holds up to `cap` points (the whole history window); a box is `cols` columns wide, // and a narrow one — the net graph, a phone — has fewer columns than points. Taking "the // nearest point" for each column draws one point in three and, worse, a *different* one in // three every time the curve advances: a spike is there, gone, there again, and the whole // graph reshapes once a second. So: // // - a column draws the PEAK of all the points that fall in it, so nothing is ever skipped; and // - columns are cut from the data, not from the box: point number `a` (counted since the page // opened, `advance` being the newest) always belongs to column ⌊a·cols/cap⌋, whatever has // arrived since. A finished column therefore never changes — the graph only ever slides // left by whole columns, and only its newest column is still moving. // // With more columns than points (a wide box) each point simply spans several columns. // Returns one value per column, oldest first, right-aligned: a young curve has fewer columns // than the box and draws in from the right. export function columnsOf(values: number[], advance: number, cap: number, cols: number): number[] { return columnsAt(values, advance, advance + 1, cap, cols).cols.slice(-cols) } // The same columns, placed in time. `now` is how far the curve has got in points: `advance` // plus the share of the newest point's stride already gone (0–1], so a curve whose points are // several ticks apart still moves on every tick instead of jumping once a point. `edge` is how // much of the newest column is showing (0–1]: the box draws it that far in from the right and // everything else behind it, so the whole graph slides a fraction of a column each tick. One // more column than the box holds comes back, to fill the sliver the slide opens on the left. // `last` is the newest column's number, for a caller that follows a column as it slides. export function columnsAt(values: number[], advance: number, now: number, cap: number, cols: number): { cols: number[]; edge: number; last: number } { const n = Math.min(values.length, cap) if (n === 0 || cols < 1 || cap < 1) return { cols: [], edge: 1, last: 0 } const at = (a: number) => values[values.length - 1 - (advance - a)] // the point numbered a const oldest = advance - n + 1 const r = cols / cap const out: number[] = [] if (cols >= cap) { const kLast = Math.ceil(now * r) - 1 for (let k = kLast - cols; k <= kLast; k++) { const a = Math.floor((k * cap) / cols) if (a >= oldest && a <= advance) out.push(at(a)) } return { cols: out, edge: Math.min(1, Math.max(0, now * r - kLast)) || 1, last: kLast } } // the newest column is the newest point's; until the next point lands in the next column the // slide waits at its edge, rather than show a column with nothing in it const kLast = Math.floor((advance * cols) / cap) for (let k = kLast - cols; k <= kLast; k++) { const a0 = Math.max(oldest, Math.ceil((k * cap) / cols)), a1 = Math.min(advance, Math.ceil(((k + 1) * cap) / cols) - 1) if (a1 < a0) continue let peak = at(a0) for (let a = a0 + 1; a <= a1; a++) peak = Math.max(peak, at(a)) out.push(peak) } return { cols: out, edge: Math.min(1, Math.max(0, now * r - kLast)) || 1, last: kLast } }