/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
timeseries-compressibility / src / components / CompressionChart.tsx
268 lines · 8.5 KBCodeBlameHistory
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 1import { useRef, useState } from 'react'
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 2import type { BoundResult, CodecResult } from '../compress/codecs'
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 3import { useWidth } from './useWidth'
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 5const GROUPS = ['no prefilter', 'delta', 'LPC']
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 6const CODER_NAMES = ['zlib', 'zstd', 'ANS']
7const CODER_VARS = ['var(--series-1)', 'var(--series-2)', 'var(--series-3)']
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 8const BOUND_LABEL = 'entropy limit'
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 10const LABEL_W = 100
12const AXIS_H = 26
14const ROW_H = 24
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 16const ROWS_PER_GROUP = 4
18type Metric = 'bits' | 'ratio'
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 20/** One drawn bar: a measured codec size, or the entropy limit for its group. */
21interface Row {
22 key: string
23 label: string
24 name: string
25 note: string
26 bytes: number
27 bitsPerSample: number
28 ratio: number
29 isBound: boolean
30 color: string
34 x: number
35 y: number
39function axisTicks(max: number): number[] {
40 const step = max > 24 ? 8 : max > 12 ? 4 : max > 6 ? 2 : max > 3 ? 1 : 0.5
41 const out: number[] = []
42 for (let v = 0; v <= max + 1e-9; v += step) out.push(v)
43 return out
46/** A bar whose data-end is rounded (4px) while the baseline end stays square. */
47function barPath(x0: number, y: number, len: number, h: number): string {
48 const r = Math.min(4, len)
49 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`
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 52/** Codec rows then the entropy limit, group by group. */
53function buildRows(results: CodecResult[], bounds: BoundResult[]): Row[] {
54 const rows: Row[] = []
55 GROUPS.forEach((group, g) => {
56 CODER_NAMES.forEach((coder, c) => {
57 const r = results[g * 3 + c]
58 if (!r) return
59 rows.push({
60 key: r.codec,
61 label: coder,
62 name: r.codec,
63 note: r.note,
64 bytes: r.bytes,
65 bitsPerSample: r.bitsPerSample,
66 ratio: r.ratio,
67 isBound: false,
68 color: CODER_VARS[c],
69 })
70 })
71 const b = bounds.find(x => x.group === group)
72 if (b) {
73 rows.push({
74 key: `${group}-bound`,
75 label: BOUND_LABEL,
76 name: `${BOUND_LABEL} (${group})`,
77 note: b.note,
78 bytes: b.bytes,
79 bitsPerSample: b.bitsPerSample,
80 ratio: b.ratio,
81 isBound: true,
82 color: 'var(--muted)',
83 })
84 }
85 })
86 return rows
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 89export default function CompressionChart(props: {
90 results: CodecResult[]
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 91 bounds: BoundResult[]
93}) {
94 const ref = useRef<HTMLDivElement>(null)
95 const width = useWidth(ref, 720)
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 96 const [metric, setMetric] = useState<Metric>('ratio')
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 97 const [tip, setTip] = useState<Tip | null>(null)
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 98 const [hovered, setHovered] = useState<string | null>(null)
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 100 const { results, bounds } = props
102 return <p className="card-note">Computing compression on the first block…</p>
103 }
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 105 const rows = buildRows(results, bounds)
106 const value = (r: { bitsPerSample: number; ratio: number }) =>
107 metric === 'bits' ? r.bitsPerSample : r.ratio
43704d2Clear the analytic rate: R now comes from the timeseries-entropy estimatorJeremy Magland 109 metric === 'bits' ? Math.max(16, ...rows.map(value)) * 1.02 : Math.max(...rows.map(value)) * 1.1
111 const plotW = width - LABEL_W - RIGHT_PAD
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 112 const height = AXIS_H + GROUPS.length * (GROUP_H + ROWS_PER_GROUP * ROW_H) + 6
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 113 const xOf = (v: number) => LABEL_W + (v / xMax) * plotW
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 114 const rowY = (i: number) =>
115 AXIS_H +
116 Math.floor(i / ROWS_PER_GROUP) * (GROUP_H + ROWS_PER_GROUP * ROW_H) +
117 GROUP_H +
118 (i % ROWS_PER_GROUP) * ROW_H
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 120 const fmt = (r: Row) => (metric === 'bits' ? r.bitsPerSample.toFixed(2) : `${r.ratio.toFixed(2)}×`)
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 122 const onBarMove = (e: React.PointerEvent, row: Row) => {
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 123 const box = ref.current!.getBoundingClientRect()
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 124 setTip({ x: e.clientX - box.left, y: e.clientY - box.top, row })
127 return (
128 <div>
129 <div className="chart-header">
130 <div>
131 <div className="segmented" role="group" aria-label="metric">
132 <button className={metric === 'ratio' ? 'active' : ''} onClick={() => setMetric('ratio')}>
133 compression ratio
134 </button>
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 135 <button className={metric === 'bits' ? 'active' : ''} onClick={() => setMetric('bits')}>
136 bits / sample
137 </button>
139 <span className="metric-hint">
140 {metric === 'bits' ? 'lower is better' : 'vs int16 — higher is better'}
141 </span>
142 </div>
143 <div className="legend">
144 {CODER_NAMES.map((name, i) => (
145 <span key={name}>
146 <span className="swatch" style={{ background: CODER_VARS[i] }} />
147 {name}
148 </span>
149 ))}
151 <span className="swatch hollow" />
152 entropy limit (not achieved)
153 </span>
155 </div>
156 <div className={`chart-body${props.computing ? ' computing' : ''}`} ref={ref}>
157 {props.computing && <span className="computing-badge">computing…</span>}
158 <svg width={width} height={height}>
159 {axisTicks(xMax).map(v => (
160 <g key={v}>
161 <line x1={xOf(v)} x2={xOf(v)} y1={AXIS_H - 6} y2={height - 4} stroke="var(--grid)" strokeWidth={1} />
162 <text x={xOf(v)} y={AXIS_H - 10} textAnchor="middle" className="axis-tick">
163 {+v.toFixed(1)}
164 </text>
165 </g>
166 ))}
167 <line x1={xOf(0)} x2={xOf(0)} y1={AXIS_H - 6} y2={height - 4} stroke="var(--baseline)" strokeWidth={1} />
168 {GROUPS.map((g, gi) => (
170 key={g}
171 x={0}
172 y={AXIS_H + gi * (GROUP_H + ROWS_PER_GROUP * ROW_H) + 15}
173 className="bar-group-label"
174 >
176 </text>
177 ))}
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 178 {rows.map((r, i) => {
180 const len = Math.max(1, (value(r) / xMax) * plotW)
181 return (
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 182 <g key={r.key} opacity={hovered === null || hovered === r.key ? 1 : 0.45}>
183 <text
184 x={8}
185 y={y + BAR_H / 2 + 4}
186 className={r.isBound ? 'bar-row-label bound' : 'bar-row-label'}
187 >
188 {r.label}
191 // Hollow: a limit nobody reached, not a measured size.
192 <path
193 d={barPath(xOf(0), y + 1, len, BAR_H - 2)}
194 fill="var(--muted)"
195 fillOpacity={0.12}
196 stroke="var(--muted)"
197 strokeWidth={1.25}
198 />
199 ) : (
200 <path d={barPath(xOf(0), y, len, BAR_H)} fill={r.color} />
201 )}
202 <text
203 x={xOf(0) + len + 6}
204 y={y + BAR_H / 2 + 4}
205 className={r.isBound ? 'bar-value bound' : 'bar-value'}
206 >
207 {fmt(r)}
209 <rect
210 x={0}
211 y={y - (ROW_H - BAR_H) / 2}
212 width={width}
213 height={ROW_H}
214 fill="transparent"
215 onPointerMove={e => {
218 }}
219 onPointerLeave={() => {
220 setHovered(null)
221 setTip(null)
222 }}
223 />
224 </g>
225 )
226 })}
227 </svg>
228 {tip && (
229 <div className="viz-tooltip" style={{ left: tip.x + 14, top: tip.y - 8 }}>
230 <div>
231 <span className="tip-value">
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 232 {tip.row.bitsPerSample.toFixed(3)} bits/sample · {tip.row.ratio.toFixed(2)}×
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 234 <span className="tip-label">{tip.row.name}</span>
236 <div className="tip-label">
355ddbbAdd per-group entropy limit bars to the compression chartJeremy Magland 237 {tip.row.isBound ? 'equivalent to ' : ''}
238 {tip.row.bytes.toLocaleString()} bytes · {tip.row.note}
240 </div>
241 )}
242 </div>
243 <details className="chart-table">
244 <summary>Table view</summary>
245 <table>
246 <thead>
247 <tr>
248 <th>method</th>
249 <th>bytes</th>
250 <th>bits/sample</th>
251 <th>ratio vs int16</th>
252 </tr>
253 </thead>
254 <tbody>
256 <tr key={r.key}>
257 <td>{r.name}</td>
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 258 <td>{r.bytes.toLocaleString()}</td>
259 <td>{r.bitsPerSample.toFixed(3)}</td>
260 <td>{r.ratio.toFixed(3)}</td>
261 </tr>
262 ))}
263 </tbody>
264 </table>
265 </details>
266 </div>
267 )
moveopenescclose