/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
Make the signal view stationary by default with a play toggle
Jeremy Magland <jmagland@flatironinstitute.org> committed commit eb36f3fe3241 parent 36e8cea Browse files
4 changed files+52−13
README.mdmodified+3−2View file
@@ -5,8 +5,9 @@ Interactive exploration of how compressible quantized time series are.
55 The generating model is: i.i.d. Gaussian noise (std σ, measured in quantization
66 steps) → FIR filter → optional additive uniform dither on [-½, ½) → round to
77 integers. The app shows the filter (convolution kernel and frequency response,
8-with cutoffs in Hz against a chosen sample rate), an endlessly scrolling view of
9-the generated integer signal, and the measured compression of a 120,000-sample
8+with cutoffs in Hz against a chosen sample rate), a window of the generated
9+integer signal (stationary by default, with a play toggle to let it stream
10+endlessly), and the measured compression of a 120,000-sample
1011 block under nine methods — zlib, zstd, and an rANS entropy coder, each raw,
1112 delta-coded, and LPC-residual-coded — as bits per sample and as ratio against
1213 raw int16 storage.
src/App.tsxmodified+3−2View file
@@ -118,8 +118,9 @@ export default function App() {
118118 <h2>Quantized signal z</h2>
119119 <ScrollingView kernel={kernel} sigma={sigma} dither={dither} sigmaY={sigmaY} />
120120 <p className="card-note">
121- Continuously generated from the model; sample-and-hold rendering, so the integer
122- staircase appears as σ approaches the quantization step.
121+ A window of samples from the model, redrawn when parameters change; press play to watch
122+ it stream. Sample-and-hold rendering, so the integer staircase appears as σ approaches
123+ the quantization step.
123124 </p>
124125 </section>
125126
src/app.cssmodified+22−0View file
@@ -181,12 +181,34 @@ body {
181181 height: auto;
182182 }
183183
184+.scroll-wrap {
185+ position: relative;
186+}
187+
184188 .scroll-canvas {
185189 display: block;
186190 width: 100%;
187191 height: 240px;
188192 }
189193
194+.play-btn {
195+ position: absolute;
196+ top: 8px;
197+ right: 8px;
198+ background: var(--surface);
199+ color: var(--ink-2);
200+ border: 1px solid var(--baseline);
201+ border-radius: 7px;
202+ padding: 4px 12px;
203+ font: inherit;
204+ font-size: 12px;
205+ cursor: pointer;
206+}
207+
208+.play-btn:hover {
209+ color: var(--ink);
210+}
211+
190212 .viz-tooltip {
191213 position: absolute;
192214 pointer-events: none;
src/components/ScrollingView.tsxmodified+24−9View file
@@ -1,4 +1,4 @@
1-import { useEffect, useRef } from 'react'
1+import { useEffect, useRef, useState } from 'react'
22 import { Pipeline } from '../model/pipeline'
33
44 /** Display samples generated per second; px per sample is fixed below. */
@@ -15,9 +15,10 @@ function niceStep(span: number): number {
1515 }
1616
1717 /**
18- * The endlessly generated quantized signal z, drawn sample-and-hold so the
19- * integer staircase is visible once σ is small. Rendering is a canvas ring
20- * buffer fed by the same Pipeline the compression worker uses.
18+ * The generated quantized signal z, drawn sample-and-hold so the integer
19+ * staircase is visible once σ is small. Rendering is a canvas ring buffer fed
20+ * by the same Pipeline the compression worker uses. Stationary by default — a
21+ * fresh window per parameter change — with a play toggle to let it stream.
2122 */
2223 export default function ScrollingView(props: {
2324 kernel: Float64Array
@@ -28,6 +29,9 @@ export default function ScrollingView(props: {
2829 }) {
2930 const canvasRef = useRef<HTMLCanvasElement>(null)
3031 const seedRef = useRef(1)
32+ const [playing, setPlaying] = useState(false)
33+ const playingRef = useRef(playing)
34+ playingRef.current = playing
3135
3236 useEffect(() => {
3337 const canvas = canvasRef.current
@@ -68,10 +72,14 @@ export default function ScrollingView(props: {
6872 raf = requestAnimationFrame(draw)
6973 const dt = Math.min(0.25, (now - last) / 1000)
7074 last = now
71- carry += dt * RATE
72- const n = Math.floor(carry)
73- carry -= n
74- if (n > 0) push(pipeline.next(n))
75+ if (playingRef.current) {
76+ carry += dt * RATE
77+ const n = Math.floor(carry)
78+ carry -= n
79+ if (n > 0) push(pipeline.next(n))
80+ } else {
81+ carry = 0
82+ }
7583
7684 const dpr = window.devicePixelRatio || 1
7785 const w = canvas.clientWidth
@@ -142,5 +150,12 @@ export default function ScrollingView(props: {
142150 }
143151 }, [props.kernel, props.sigma, props.dither, props.sigmaY])
144152
145- return <canvas ref={canvasRef} className="scroll-canvas" />
153+ return (
154+ <div className="scroll-wrap">
155+ <canvas ref={canvasRef} className="scroll-canvas" />
156+ <button className="play-btn" onClick={() => setPlaying(p => !p)}>
157+ {playing ? '⏸ pause' : '▶ play'}
158+ </button>
159+ </div>
160+ )
146161 }
moveopenescclose