36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 1import { useRef, useState } from 'react'
2import type { CodecResult } from '../compress/codecs'
3import { useWidth } from './useWidth'
5const GROUPS = ['no prefilter', 'delta', `LPC`]
6const CODER_NAMES = ['zlib', 'zstd', 'ANS']
7const CODER_VARS = ['var(--series-1)', 'var(--series-2)', 'var(--series-3)']
9const LABEL_W = 96
10const RIGHT_PAD = 64
11const AXIS_H = 26
12const GROUP_H = 22
13const ROW_H = 26
14const BAR_H = 16
16type Metric = 'bits' | 'ratio'
18interface Tip {
19 x: number
20 y: number
21 result: CodecResult
22}
24function axisTicks(max: number): number[] {
25 const step = max > 24 ? 8 : max > 12 ? 4 : max > 6 ? 2 : max > 3 ? 1 : 0.5
26 const out: number[] = []
27 for (let v = 0; v <= max + 1e-9; v += step) out.push(v)
28 return out
29}
31/** A bar whose data-end is rounded (4px) while the baseline end stays square. */
32function barPath(x0: number, y: number, len: number, h: number): string {
33 const r = Math.min(4, len)
34 return `M${x0},${y} h${len - r} a${r},${r} 0 0 1 ${r},${r} v${h - 2 * r} a${r},${r} 0 0 1 ${-r},${r} h${-(len - r)} z`
35}
37export default function CompressionChart(props: {
38 results: CodecResult[]
39 theoryBits: number
40 computing: boolean
41}) {
42 const ref = useRef<HTMLDivElement>(null)
43 const width = useWidth(ref, 720)
44 const [metric, setMetric] = useState<Metric>('bits')
45 const [tip, setTip] = useState<Tip | null>(null)
46 const [hovered, setHovered] = useState<number | null>(null)
48 const { results, theoryBits } = props
49 if (results.length === 0) {
50 return <p className="card-note">Computing compression on the first block…</p>
51 }
53 const value = (r: CodecResult) => (metric === 'bits' ? r.bitsPerSample : r.ratio)
54 const theoryValue = metric === 'bits' ? theoryBits : theoryBits > 0 ? 16 / theoryBits : NaN
55 const theoryVisible = Number.isFinite(theoryValue) && theoryValue > 0
56 const xMax =
57 metric === 'bits'
58 ? Math.max(16, ...results.map(value), theoryVisible ? theoryValue : 0) * 1.02
59 : Math.max(...results.map(value), theoryVisible ? theoryValue : 0) * 1.1
61 const plotW = width - LABEL_W - RIGHT_PAD
62 const height = AXIS_H + GROUPS.length * (GROUP_H + 3 * ROW_H) + 6
63 const xOf = (v: number) => LABEL_W + (v / xMax) * plotW
64 const rowY = (i: number) => AXIS_H + Math.floor(i / 3) * (GROUP_H + 3 * ROW_H) + GROUP_H + (i % 3) * ROW_H
66 const fmt = (r: CodecResult) =>
67 metric === 'bits' ? r.bitsPerSample.toFixed(2) : `${r.ratio.toFixed(2)}×`
69 const onBarMove = (e: React.PointerEvent, r: CodecResult) => {
70 const box = ref.current!.getBoundingClientRect()
71 setTip({ x: e.clientX - box.left, y: e.clientY - box.top, result: r })
72 }
74 const theoryX = theoryVisible ? xOf(theoryValue) : 0
75 const theoryLabel =
76 metric === 'bits' ? `entropy rate R = ${theoryBits.toFixed(2)}` : `R ⇒ ${(16 / theoryBits).toFixed(2)}×`
78 return (
79 <div>
80 <div className="chart-header">
81 <div>
82 <div className="segmented" role="group" aria-label="metric">
83 <button className={metric === 'bits' ? 'active' : ''} onClick={() => setMetric('bits')}>
84 bits / sample
85 </button>
86 <button className={metric === 'ratio' ? 'active' : ''} onClick={() => setMetric('ratio')}>
87 compression ratio
88 </button>
89 </div>
90 <span className="metric-hint">
91 {metric === 'bits' ? 'lower is better' : 'vs int16 — higher is better'}
92 </span>
93 </div>
94 <div className="legend">
95 {CODER_NAMES.map((name, i) => (
96 <span key={name}>
97 <span className="swatch" style={{ background: CODER_VARS[i] }} />
98 {name}
99 </span>
100 ))}
101 </div>
102 </div>
103 <div className={`chart-body${props.computing ? ' computing' : ''}`} ref={ref}>
104 {props.computing && <span className="computing-badge">computing…</span>}
105 <svg width={width} height={height}>
106 {axisTicks(xMax).map(v => (
107 <g key={v}>
108 <line x1={xOf(v)} x2={xOf(v)} y1={AXIS_H - 6} y2={height - 4} stroke="var(--grid)" strokeWidth={1} />
109 <text x={xOf(v)} y={AXIS_H - 10} textAnchor="middle" className="axis-tick">
110 {+v.toFixed(1)}
111 </text>
112 </g>
113 ))}
114 <line x1={xOf(0)} x2={xOf(0)} y1={AXIS_H - 6} y2={height - 4} stroke="var(--baseline)" strokeWidth={1} />
115 {GROUPS.map((g, gi) => (
116 <text key={g} x={0} y={AXIS_H + gi * (GROUP_H + 3 * ROW_H) + 15} className="bar-group-label">
117 {g}
118 </text>
119 ))}
120 {results.map((r, i) => {
121 const y = rowY(i)
122 const len = Math.max(1, (value(r) / xMax) * plotW)
123 const label = fmt(r)
124 return (
125 <g key={r.codec} opacity={hovered === null || hovered === i ? 1 : 0.45}>
126 <text x={8} y={y + BAR_H / 2 + 4} className="bar-row-label">
127 {CODER_NAMES[i % 3]}
128 </text>
129 <path d={barPath(xOf(0), y, len, BAR_H)} fill={CODER_VARS[i % 3]} />
130 <text x={xOf(0) + len + 6} y={y + BAR_H / 2 + 4} className="bar-value">
131 {label}
132 </text>
133 <rect
134 x={0}
135 y={y - (ROW_H - BAR_H) / 2}
136 width={width}
137 height={ROW_H}
138 fill="transparent"
139 onPointerMove={e => {
140 setHovered(i)
141 onBarMove(e, r)
142 }}
143 onPointerLeave={() => {
144 setHovered(null)
145 setTip(null)
146 }}
147 />
148 </g>
149 )
150 })}
151 {theoryVisible && (
152 <g>
153 <line
154 x1={theoryX}
155 x2={theoryX}
156 y1={AXIS_H - 2}
157 y2={height - 4}
158 stroke="var(--ink-2)"
159 strokeWidth={1.5}
160 strokeDasharray="5 4"
161 />
162 <text
163 x={theoryX + (theoryX > width - 150 ? -6 : 6)}
164 y={AXIS_H + 10}
165 textAnchor={theoryX > width - 150 ? 'end' : 'start'}
166 className="bar-value"
167 fill="var(--ink)"
168 >
169 {theoryLabel}
170 </text>
171 </g>
172 )}
173 </svg>
174 {tip && (
175 <div className="viz-tooltip" style={{ left: tip.x + 14, top: tip.y - 8 }}>
176 <div>
177 <span className="tip-value">
178 {tip.result.bitsPerSample.toFixed(3)} bits/sample · {tip.result.ratio.toFixed(2)}×
179 </span>{' '}
180 <span className="tip-label">{tip.result.codec}</span>
181 </div>
182 <div className="tip-label">
183 {tip.result.bytes.toLocaleString()} bytes · {tip.result.note}
184 </div>
185 </div>
186 )}
187 </div>
188 <details className="chart-table">
189 <summary>Table view</summary>
190 <table>
191 <thead>
192 <tr>
193 <th>method</th>
194 <th>bytes</th>
195 <th>bits/sample</th>
196 <th>ratio vs int16</th>
197 </tr>
198 </thead>
199 <tbody>
200 {results.map(r => (
201 <tr key={r.codec}>
202 <td>{r.codec}</td>
203 <td>{r.bytes.toLocaleString()}</td>
204 <td>{r.bitsPerSample.toFixed(3)}</td>
205 <td>{r.ratio.toFixed(3)}</td>
206 </tr>
207 ))}
208 {theoryBits > 0 && (
209 <tr>
210 <td>theory: entropy rate R</td>
211 <td>—</td>
212 <td>{theoryBits.toFixed(3)}</td>
213 <td>{(16 / theoryBits).toFixed(3)}</td>
214 </tr>
215 )}
216 </tbody>
217 </table>
218 </details>
219 </div>
220 )
221}