jzxiao commited on
Commit
fe81ad7
·
verified ·
1 Parent(s): bfe13e2

Add weighted composite scores and average dataset ranks

Browse files

Add positive one-decimal a/b/c weights summing to 1.0, Overall composite scores, mean per-dataset fractional ranks, and reproducible CSV fields. Document DBP kernel timing and float reconstruction limits. Measurement records and aggregates are unchanged.

Files changed (9) hide show
  1. README.md +4 -1
  2. app.js +29 -10
  3. data.json +2 -2
  4. export_results.py +14 -3
  5. index.html +9 -2
  6. manifest.json +1 -1
  7. ranking.js +58 -0
  8. style.css +2 -0
  9. ui-data.json +0 -0
README.md CHANGED
@@ -24,7 +24,7 @@ python3 deploy/huggingface-compressionbenchmark/export_results.py
24
  python3 deploy/huggingface-compressionbenchmark/build_ui_payload.py
25
  ```
26
 
27
- Publish `README.md`, `index.html`, `style.css`, `app.js`, `data.json`, `ui-data.json`, `manifest.json`, `export_results.py`, and `build_ui_payload.py` to the static Space. The manifest records the exact source-file identities and metric boundaries. The compact browser payload retains the full snapshot's measurements and SHA-256 identity.
28
 
29
  Only measurements in the authoritative current project scope are exported. Missing metrics and missing coverage are displayed explicitly. Existing results do not imply independently verified round trips or uniform hardware environments unless those checks are recorded in the source.
30
 
@@ -34,7 +34,10 @@ Only measurements in the authoritative current project scope are exported. Missi
34
  - Sortable metrics, optional columns, and CSV download of the selected ranking
35
  - Compression rate versus encoding/decoding cost plots
36
  - Dataset-level method details, snapshot provenance, and metric definitions
 
37
 
38
  The default family view selects the best measured compression-rate configuration independently for each dataset. Switch to **Exact configuration** to hold a configuration fixed. Runtime results stay separate. The paper snapshot tab preserves the frozen source table and is separate from the current report aggregation.
39
 
 
 
40
  Source project: https://github.com/xjz17/web_compression
 
24
  python3 deploy/huggingface-compressionbenchmark/build_ui_payload.py
25
  ```
26
 
27
+ Publish `README.md`, `index.html`, `style.css`, `app.js`, `ranking.js`, `data.json`, `ui-data.json`, `manifest.json`, `export_results.py`, and `build_ui_payload.py` to the static Space. The manifest records the exact source-file identities and metric boundaries. The compact browser payload retains the full snapshot's measurements and SHA-256 identity.
28
 
29
  Only measurements in the authoritative current project scope are exported. Missing metrics and missing coverage are displayed explicitly. Existing results do not imply independently verified round trips or uniform hardware environments unless those checks are recorded in the source.
30
 
 
34
  - Sortable metrics, optional columns, and CSV download of the selected ranking
35
  - Compression rate versus encoding/decoding cost plots
36
  - Dataset-level method details, snapshot provenance, and metric definitions
37
+ - Overall composite score and average dataset Rank, with 36 valid positive one-decimal weight combinations
38
 
39
  The default family view selects the best measured compression-rate configuration independently for each dataset. Switch to **Exact configuration** to hold a configuration fixed. Runtime results stay separate. The paper snapshot tab preserves the frozen source table and is separate from the current report aggregation.
40
 
41
+ The Overall score is `a × average rate + b × normalized average encode time + c × normalized average decode time`. Time normalization is linear min–max across all eligible rows in the selected runtime, scope and row mode; an equal-time range normalizes to zero. Search, family and coverage filters only hide rows and never change the reference. Average Rank averages per-dataset composite-score ranks with dataset-local time normalization and fractional ties. Missing results are omitted. Coverage and candidate counts can differ across datasets; method details expose dataset scores, ranks and candidate counts. CSV downloads include weights, normalization bounds and rank coverage. The default weights are `a=0.4, b=0.3, c=0.3`.
42
+
43
  Source project: https://github.com/xjz17/web_compression
app.js CHANGED
@@ -1,3 +1,4 @@
 
1
  const $ = (id) => document.getElementById(id);
2
  const escape = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
3
  const number = (v) => v != null && Number.isFinite(Number(v)) ? Number(v) : null;
@@ -6,10 +7,15 @@ const fmt = (v, digits=3) => number(v) === null ? 'Not reported' : Number(v).toL
6
  const countFmt = (v) => Number(v).toLocaleString('en-US');
7
  const runtimeName = (v) => ({cpp:'C++',java:'Java',python:'Python'}[v] || v);
8
  const cols = [
 
 
 
9
  {key:'compression_rate',label:'Average rate ↓',digits:4,visible:true},
10
  {key:'overall_rate',label:'Overall rate ↓',digits:4,visible:false},
11
  {key:'compression_time',label:'Encode ns/point ↓',digits:2,visible:true},
12
  {key:'decompression_time',label:'Decode ns/point ↓',digits:2,visible:true},
 
 
13
  {key:'compressed_bytes',label:'Compressed bytes',digits:0,visible:false},
14
  {key:'original_bytes',label:'Original bytes',digits:0,visible:false},
15
  ];
@@ -44,7 +50,8 @@ function aggregate(input) {
44
  const original=hasBytes?source.reduce((s,r)=>s+r.original_bytes,0):null, compressed=hasBytes?source.reduce((s,r)=>s+r.compressed_bytes,0):null;
45
  rows.push({method,family:source[0].family,runtime:source[0].runtime,compression_rate:mean(source.map(r=>r.compression_rate)),compression_time:mean(source.map(r=>r.compression_time)),decompression_time:mean(source.map(r=>r.decompression_time)),overall_rate:hasBytes?compressed/original:null,original_bytes:original,compressed_bytes:compressed,coverage:new Set(source.map(r=>r.dataset)).size,totalDatasets,source});
46
  }
47
- return rows.filter(r=>!state().complete||r.coverage===totalDatasets).sort((a,b)=>{
 
48
  const av=number(a[sort.key]),bv=number(b[sort.key]);
49
  if(av===null&&bv===null)return a.method.localeCompare(b.method);
50
  if(av===null)return 1;if(bv===null)return -1;
@@ -52,10 +59,10 @@ function aggregate(input) {
52
  });
53
  }
54
  function renderTable() {
55
- const visible=cols.filter(c=>c.visible);
56
  $('leaderboard').querySelector('thead').innerHTML=`<tr><th>Position</th><th>Method / configuration</th>${visible.map(c=>`<th aria-sort="${sort.key===c.key?(sort.direction===1?'ascending':'descending'):'none'}"><button data-sort="${c.key}" title="Sort ${escape(c.label)}">${escape(c.label)}${sort.key===c.key?(sort.direction===1?' ▴':' ▾'):''}</button></th>`).join('')}<th>Dataset coverage</th></tr>`;
57
  const minima=Object.fromEntries(visible.map(c=>[c.key,Math.min(...currentRows.map(r=>number(r[c.key])).filter(n=>n!==null))]));
58
- $('leaderboard').querySelector('tbody').innerHTML=currentRows.length?currentRows.map((r,i)=>`<tr><td class="rank numeric">${i+1}</td><td><button class="method-button" data-method="${escape(r.method)}">${escape(r.method)}</button><span class="method-family">${escape(r.family)} · ${escape(runtimeName(r.runtime))}</span></td>${visible.map(c=>`<td class="numeric ${!c.key.endsWith('bytes')&&number(r[c.key])!==null&&r[c.key]===minima[c.key]?'best':''}">${fmt(r[c.key],c.digits)}</td>`).join('')}<td><span class="coverage ${r.coverage===r.totalDatasets?'full':''}">${r.coverage} / ${r.totalDatasets}</span></td></tr>`).join(''):`<tr><td colspan="${visible.length+3}" class="empty-cell">No measurements match this selection. Try another runtime, data type, or dataset.</td></tr>`;
59
  for(const button of $('leaderboard').querySelectorAll('[data-sort]'))button.addEventListener('click',()=>{const key=button.dataset.sort;sort={key,direction:sort.key===key?-sort.direction:1};refresh();});
60
  for(const button of $('leaderboard').querySelectorAll('[data-method]'))button.addEventListener('click',()=>showMethod(button.dataset.method,true));
61
  $('row-count').textContent=`${currentRows.length} ${state().mode==='family'?'method families':'method configurations'} · sorted by ${cols.find(c=>c.key===sort.key)?.label??sort.key}`;
@@ -69,7 +76,12 @@ function refresh() {
69
  // preaggregated fixed/non-fixed metrics and call it an all-float result.
70
  $('results-note').textContent='All-float measurements are not available in this snapshot. Select a precision category.';
71
  } else $('results-note').textContent=state().mode==='family'?'Each family uses its lowest-rate measured configuration per dataset. Configurations may differ across datasets; use Exact configuration for fixed parameters.':'Each row retains one exact configuration. Lower compression rates and lower ns/point are better. Coverage counts datasets, not identical column sets.';
72
- currentRows=aggregate(filteredRecords());
 
 
 
 
 
73
  const s=state(),scope=scopeKey(),ds=s.dataset==='all'?'all measured datasets':s.dataset;
74
  const names={overall:'all numeric columns',integer:'integer columns',float:'floating-point columns',fixed_float:'fixed-precision float columns',nonfixed_float:'non-fixed-precision float columns'};
75
  $('selection-summary').textContent=`${runtimeName(s.runtime)} · ${names[scope]||scope} · ${ds} · ${currentRows.length} methods`;
@@ -82,6 +94,9 @@ function setTab(tab) {
82
  activeTab=['overall','dataset','tradeoffs','methods','paper','about'].includes(tab)?tab:'overall';
83
  for(const b of document.querySelectorAll('[data-tab]')){b.classList.toggle('active',b.dataset.tab===activeTab);if(b.dataset.tab===activeTab)b.setAttribute('aria-current','page');else b.removeAttribute('aria-current');}
84
  $('results-view').hidden=['methods','paper','about'].includes(activeTab);$('methods-view').hidden=activeTab!=='methods';$('paper-view').hidden=activeTab!=='paper';$('about-view').hidden=activeTab!=='about';$('chart-view').hidden=activeTab!=='tradeoffs';
 
 
 
85
  if(activeTab==='dataset'&&$('dataset').value==='all'){const first=[...datasets.keys()].find(k=>records.some(r=>r.dataset===k&&r.runtime===$('runtime').value));if(first)$('dataset').value=first;}
86
  if(activeTab==='overall')$('dataset').value='all';
87
  refresh();
@@ -89,9 +104,9 @@ function setTab(tab) {
89
  function showMethod(method,focus=false) {
90
  const row=currentRows.find(r=>r.method===method), detail=$('method-detail');
91
  if(!row){selectedMethod=null;detail.hidden=true;return;}selectedMethod=method;
92
- const meta=methods.get(method)??methods.get(row.source[0].configuration), pipeline=meta?.operator_pipeline;
93
  const source=row.source.slice().sort((a,b)=>a.dataset.localeCompare(b.dataset));
94
- detail.innerHTML=`<div class="detail-head"><div><h2 id="detail-title">${escape(method)}</h2><p class="muted">${escape(row.family)} · ${escape(runtimeName(row.runtime))} · ${row.coverage} measured datasets</p></div><button id="close-detail" class="quiet-button">Close details</button></div><div class="detail-grid"><div><strong>${fmt(row.compression_rate,4)}</strong><span>average compression rate</span></div><div><strong>${fmt(row.compression_time,2)}</strong><span>encoding ns/point</span></div><div><strong>${fmt(row.decompression_time,2)}</strong><span>decoding ns/point</span></div><div><strong>${row.coverage} / ${row.totalDatasets}</strong><span>dataset coverage</span></div></div>${pipeline?`<p class="source-code">Interpretive operator mapping: ${escape(typeof pipeline==='string'?pipeline:JSON.stringify(pipeline))}</p>`:''}<p class="muted">Dataset metrics below come from the published result files. Independent round-trip evidence and hardware are ${snapshot.benchmark?.roundtrip_evidence?'documented in the manifest':'not reported for this snapshot'}.</p><div class="detail-table"><table><thead><tr><th>Dataset</th><th>Configuration</th><th>Rate ↓</th><th>Encode ns/point ↓</th><th>Decode ns/point ↓</th><th>Original bytes</th><th>Compressed bytes</th></tr></thead><tbody>${source.map(r=>`<tr><td>${escape(r.dataset)}</td><td>${escape(r.configuration)}</td><td class="numeric">${fmt(r.compression_rate,4)}</td><td class="numeric">${fmt(r.compression_time,2)}</td><td class="numeric">${fmt(r.decompression_time,2)}</td><td class="numeric">${fmt(r.original_bytes,0)}</td><td class="numeric">${fmt(r.compressed_bytes,0)}</td></tr>`).join('')}</tbody></table></div>`;
95
  detail.hidden=false;$('close-detail').addEventListener('click',()=>{detail.hidden=true;selectedMethod=null;});
96
  if(focus){detail.scrollIntoView({block:'nearest',behavior:'auto'});detail.focus({preventScroll:true});}
97
  }
@@ -113,14 +128,16 @@ function renderChart() {
113
  }
114
  function csv() {
115
  const quote=v=>{let text=String(v??'');if(typeof v==='string'&&/^[=+@-]/.test(text))text="'"+text;return `"${text.replace(/"/g,'""')}"`;};
116
- const columns=['position','method','runtime','scope','row_mode','selected_dataset','sort_metric','sort_direction','average_compression_rate','overall_compression_rate','encode_ns_per_point','decode_ns_per_point','dataset_coverage','datasets_in_scope','selected_configuration_by_dataset'];
117
- const body=currentRows.map((r,i)=>[i+1,r.method,r.runtime,scopeKey(),state().mode,state().dataset,sort.key,sort.direction===1?'ascending':'descending',r.compression_rate,r.overall_rate,r.compression_time,r.decompression_time,r.coverage,r.totalDatasets,JSON.stringify(Object.fromEntries(r.source.map(s=>[s.dataset,s.configuration])))].map(quote).join(','));
 
118
  const url=URL.createObjectURL(new Blob(['\ufeff'+[columns.join(','),...body].join('\r\n')],{type:'text/csv;charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download=`THULab-compression-${state().runtime}-${scopeKey()}-${state().dataset}.csv`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
119
  }
120
  function renderAbout() {
121
  const b=snapshot.benchmark||{},s=snapshot.source||{},c=snapshot.coverage||{};
122
  const notes=[b.scope_note,b.aggregation_note,b.selection_note,...(b.notes||[]),...(b.measurement_boundaries||[])].filter(Boolean);
123
  $('methodology').innerHTML=notes.map(n=>`<p class="notice">${escape(n)}</p>`).join('');
 
124
  const definitions=b.metric_definitions||{};if(Object.keys(definitions).length)$('methodology').innerHTML+=`<dl class="definitions">${Object.entries(definitions).map(([k,v])=>`<div><dt>${escape(k.replaceAll('_',' '))}</dt><dd>${escape(typeof v==='string'?v:JSON.stringify(v))}</dd></div>`).join('')}</dl>`;
125
  const exclusions=c.exclusions||c.excluded_datasets||snapshot.exclusions||[];
126
  const info={ 'Snapshot exported':snapshot.exported_at||s.exported_at||'Not reported','Dataset scope generated':s.generated_at||'Not reported', 'Source checkout':s.git_commit||'Not reported','Source files':`${s.source_files?.length??0} metadata inputs and ${s.result_csv_files_used?.length??0} result CSVs (hashes in manifest)`,'Result entries across scopes':`${records.length} (scope slices can reuse a measurement)`,'Worktree source state':s.worktree_dirty_inputs?.length?'Includes current local changes; input file hashes identify the published snapshot.':'See manifest for source file hashes.','Runtime environment':b.hardware||'Hardware details are not recorded in this exported result snapshot.','Lossless evidence':b.roundtrip_evidence||'Lossless-design method filter from the source project; per-run round-trip verification is not recorded here.'};
@@ -141,9 +158,11 @@ function init() {
141
  $('dataset-count').textContent=countFmt(new Set(records.map(r=>r.dataset)).size);$('method-count').textContent=countFmt(new Set(records.map(r=>r.family)).size);$('runtime-count').textContent=runtimes.length;$('record-count').textContent=countFmt(snapshot.coverage?.runtime_configuration_count??new Set(records.map(r=>`${r.runtime}\0${r.method}`)).size);
142
  const exported=snapshot.exported_at||snapshot.source?.exported_at||'not reported';$('snapshot').textContent=`Result snapshot exported · ${String(exported).replace('T',' ').slice(0,19)}${exported==='not reported'?'':' UTC'}`;$('footer-snapshot').textContent=`Source inputs documented in the manifest`;
143
  $('column-options').innerHTML=cols.map(c=>`<label><input type="checkbox" data-column="${c.key}" ${c.visible?'checked':''}>${escape(c.label)}</label>`).join('');
 
 
144
  for(const input of $('column-options').querySelectorAll('input'))input.addEventListener('change',()=>{cols.find(c=>c.key===input.dataset.column).visible=input.checked;renderTable();});
145
  for(const id of ['runtime','dtype','precision','dataset','family','row-mode','complete-only'])$(id).addEventListener('change',refresh);$('search').addEventListener('input',refresh);$('chart-metric').addEventListener('change',renderChart);$('download').addEventListener('click',csv);
146
- $('reset').addEventListener('click',()=>{$('runtime').value=runtimes.includes('cpp')?'cpp':runtimes[0];$('dtype').value='overall';$('precision').value='all';$('dataset').value='all';$('row-mode').value='family';$('search').value='';$('family').value='all';$('complete-only').checked=false;selectedMethod=null;$('method-detail').hidden=true;sort={key:'compression_rate',direction:1};setTab(activeTab);});
147
  for(const tab of document.querySelectorAll('[data-tab]'))tab.addEventListener('click',()=>{history.replaceState(null,'',`#${tab.dataset.tab}`);setTab(tab.dataset.tab);});
148
  const renderPaper=()=>{const rows=(snapshot.paper_table3?.rows||[]).filter(r=>r.scope===$('paper-scope').value);$('paper-table').querySelector('tbody').innerHTML=rows.map(r=>`<tr><td>${escape(r.method)}</td><td class="numeric">${fmt(r.rank,1)}</td><td class="numeric">${fmt(r.average_compression_rate,4)}</td><td class="numeric">${fmt(r.average_compression_time_ns_per_point,1)}</td><td class="numeric">${fmt(r.average_decompression_time_ns_per_point,1)}</td><td class="numeric">${fmt(r.median_balanced_score,4)}</td></tr>`).join('');};$('paper-scope').addEventListener('change',renderPaper);renderPaper();
149
  renderAbout();setTab(location.hash.slice(1)||'overall');
@@ -157,5 +176,5 @@ function setupTheme() {
157
  $('theme').addEventListener('click',()=>{const next=document.documentElement.dataset.theme==='dark'?'light':'dark';document.documentElement.dataset.theme=next;$('theme').textContent=next==='dark'?'Light mode':'Dark mode';try{localStorage.setItem('thulab-theme',next);}catch{}});
158
  }
159
  setupTheme();
160
- try { const response=await fetch('./ui-data.json');if(!response.ok)throw new Error(`Snapshot request returned ${response.status}`);snapshot=await response.json();init(); }
161
  catch(e){$('load-error').hidden=false;$('load-error').textContent=`The result snapshot could not be loaded. Please reload the page or download the JSON from the repository. ${e.message}`;$('snapshot').textContent='Snapshot unavailable';$('leaderboard').querySelector('tbody').innerHTML='<tr><td class="empty-cell">Results unavailable.</td></tr>';}
 
1
+ import {WEIGHT_TENTHS,parseWeights,addRankingMetrics} from './ranking.js?v=20261008-rank-v1';
2
  const $ = (id) => document.getElementById(id);
3
  const escape = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
4
  const number = (v) => v != null && Number.isFinite(Number(v)) ? Number(v) : null;
 
7
  const countFmt = (v) => Number(v).toLocaleString('en-US');
8
  const runtimeName = (v) => ({cpp:'C++',java:'Java',python:'Python'}[v] || v);
9
  const cols = [
10
+ {key:'average_rank',label:'Average Rank ↓',digits:2,visible:true,overallOnly:true},
11
+ {key:'rank_coverage',label:'Rank coverage',digits:0,visible:false,overallOnly:true},
12
+ {key:'composite_score',label:'Composite score ↓',digits:4,visible:true,overallOnly:true},
13
  {key:'compression_rate',label:'Average rate ↓',digits:4,visible:true},
14
  {key:'overall_rate',label:'Overall rate ↓',digits:4,visible:false},
15
  {key:'compression_time',label:'Encode ns/point ↓',digits:2,visible:true},
16
  {key:'decompression_time',label:'Decode ns/point ↓',digits:2,visible:true},
17
+ {key:'normalized_compression_time',label:'Normalized encode ↓',digits:4,visible:false,overallOnly:true},
18
+ {key:'normalized_decompression_time',label:'Normalized decode ↓',digits:4,visible:false,overallOnly:true},
19
  {key:'compressed_bytes',label:'Compressed bytes',digits:0,visible:false},
20
  {key:'original_bytes',label:'Original bytes',digits:0,visible:false},
21
  ];
 
50
  const original=hasBytes?source.reduce((s,r)=>s+r.original_bytes,0):null, compressed=hasBytes?source.reduce((s,r)=>s+r.compressed_bytes,0):null;
51
  rows.push({method,family:source[0].family,runtime:source[0].runtime,compression_rate:mean(source.map(r=>r.compression_rate)),compression_time:mean(source.map(r=>r.compression_time)),decompression_time:mean(source.map(r=>r.decompression_time)),overall_rate:hasBytes?compressed/original:null,original_bytes:original,compressed_bytes:compressed,coverage:new Set(source.map(r=>r.dataset)).size,totalDatasets,source});
52
  }
53
+ const s=state();
54
+ return addRankingMetrics(rows,parseWeights($('weights').value)).filter(r=>(s.family==='all'||r.family===s.family)&&(!s.search||(s.mode==='family'?r.family:`${r.method} ${r.family}`).toLowerCase().includes(s.search))&&(!s.complete||r.coverage===totalDatasets)).sort((a,b)=>{
55
  const av=number(a[sort.key]),bv=number(b[sort.key]);
56
  if(av===null&&bv===null)return a.method.localeCompare(b.method);
57
  if(av===null)return 1;if(bv===null)return -1;
 
59
  });
60
  }
61
  function renderTable() {
62
+ const visible=cols.filter(c=>c.visible&&(!c.overallOnly||activeTab==='overall'));
63
  $('leaderboard').querySelector('thead').innerHTML=`<tr><th>Position</th><th>Method / configuration</th>${visible.map(c=>`<th aria-sort="${sort.key===c.key?(sort.direction===1?'ascending':'descending'):'none'}"><button data-sort="${c.key}" title="Sort ${escape(c.label)}">${escape(c.label)}${sort.key===c.key?(sort.direction===1?' ▴':' ▾'):''}</button></th>`).join('')}<th>Dataset coverage</th></tr>`;
64
  const minima=Object.fromEntries(visible.map(c=>[c.key,Math.min(...currentRows.map(r=>number(r[c.key])).filter(n=>n!==null))]));
65
+ $('leaderboard').querySelector('tbody').innerHTML=currentRows.length?currentRows.map((r,i)=>`<tr><td class="rank numeric">${i+1}</td><td><button class="method-button" data-method="${escape(r.method)}">${escape(r.method)}</button><span class="method-family">${escape(r.family)} · ${escape(runtimeName(r.runtime))}</span>${snapshot.benchmark?.method_audits?.[r.family]?`<span class="audit-badge">${escape(snapshot.benchmark.method_audits[r.family].label)}</span>`:''}</td>${visible.map(c=>`<td class="numeric ${!['compressed_bytes','original_bytes','rank_coverage'].includes(c.key)&&number(r[c.key])!==null&&r[c.key]===minima[c.key]?'best':''}">${c.key==='rank_coverage'?`${r.rank_coverage} / ${r.totalDatasets}`:fmt(r[c.key],c.digits)}</td>`).join('')}<td><span class="coverage ${r.coverage===r.totalDatasets?'full':''}">${r.coverage} / ${r.totalDatasets}</span></td></tr>`).join(''):`<tr><td colspan="${visible.length+3}" class="empty-cell">No measurements match this selection. Try another runtime, data type, or dataset.</td></tr>`;
66
  for(const button of $('leaderboard').querySelectorAll('[data-sort]'))button.addEventListener('click',()=>{const key=button.dataset.sort;sort={key,direction:sort.key===key?-sort.direction:1};refresh();});
67
  for(const button of $('leaderboard').querySelectorAll('[data-method]'))button.addEventListener('click',()=>showMethod(button.dataset.method,true));
68
  $('row-count').textContent=`${currentRows.length} ${state().mode==='family'?'method families':'method configurations'} · sorted by ${cols.find(c=>c.key===sort.key)?.label??sort.key}`;
 
76
  // preaggregated fixed/non-fixed metrics and call it an all-float result.
77
  $('results-note').textContent='All-float measurements are not available in this snapshot. Select a precision category.';
78
  } else $('results-note').textContent=state().mode==='family'?'Each family uses its lowest-rate measured configuration per dataset. Configurations may differ across datasets; use Exact configuration for fixed parameters.':'Each row retains one exact configuration. Lower compression rates and lower ns/point are better. Coverage counts datasets, not identical column sets.';
79
+ currentRows=aggregate(filteredRecords(true));
80
+ const audit=snapshot.benchmark?.method_audits?.DBP;
81
+ $('audit-notice').hidden=!audit||!currentRows.some(r=>r.family==='DBP');
82
+ $('audit-notice').textContent=audit?'DBP measurement audit: '+audit.timing+' '+audit.storage+' Float end-to-end losslessness is not established. See method details.':'';
83
+ const weights=parseWeights($('weights').value);
84
+ $('score-note').textContent=`Score = ${weights[0].toFixed(1)} × average rate + ${weights[1].toFixed(1)} × normalized encode + ${weights[2].toFixed(1)} × normalized decode. Lower is better. Time normalization: (time − minimum) / (maximum − minimum), across all measured rows in this runtime, scope and row mode. Search and visibility filters keep the reference fixed. Average Rank: mean of per-dataset score ranks with dataset-local time normalization; ties share their average position. Missing results are omitted. Dataset candidate counts differ, so compare rank coverage and inspect method details.`;
85
  const s=state(),scope=scopeKey(),ds=s.dataset==='all'?'all measured datasets':s.dataset;
86
  const names={overall:'all numeric columns',integer:'integer columns',float:'floating-point columns',fixed_float:'fixed-precision float columns',nonfixed_float:'non-fixed-precision float columns'};
87
  $('selection-summary').textContent=`${runtimeName(s.runtime)} · ${names[scope]||scope} · ${ds} · ${currentRows.length} methods`;
 
94
  activeTab=['overall','dataset','tradeoffs','methods','paper','about'].includes(tab)?tab:'overall';
95
  for(const b of document.querySelectorAll('[data-tab]')){b.classList.toggle('active',b.dataset.tab===activeTab);if(b.dataset.tab===activeTab)b.setAttribute('aria-current','page');else b.removeAttribute('aria-current');}
96
  $('results-view').hidden=['methods','paper','about'].includes(activeTab);$('methods-view').hidden=activeTab!=='methods';$('paper-view').hidden=activeTab!=='paper';$('about-view').hidden=activeTab!=='about';$('chart-view').hidden=activeTab!=='tradeoffs';
97
+ $('score-controls').hidden=activeTab!=='overall';
98
+ for(const input of $('column-options').querySelectorAll('input'))input.parentElement.hidden=Boolean(cols.find(c=>c.key===input.dataset.column)?.overallOnly&&activeTab!=='overall');
99
+ if(activeTab!=='overall'&&cols.find(c=>c.key===sort.key)?.overallOnly)sort={key:'compression_rate',direction:1};
100
  if(activeTab==='dataset'&&$('dataset').value==='all'){const first=[...datasets.keys()].find(k=>records.some(r=>r.dataset===k&&r.runtime===$('runtime').value));if(first)$('dataset').value=first;}
101
  if(activeTab==='overall')$('dataset').value='all';
102
  refresh();
 
104
  function showMethod(method,focus=false) {
105
  const row=currentRows.find(r=>r.method===method), detail=$('method-detail');
106
  if(!row){selectedMethod=null;detail.hidden=true;return;}selectedMethod=method;
107
+ const meta=methods.get(method)??methods.get(row.source[0].configuration), pipeline=meta?.operator_pipeline, audit=snapshot.benchmark?.method_audits?.[row.family];
108
  const source=row.source.slice().sort((a,b)=>a.dataset.localeCompare(b.dataset));
109
+ detail.innerHTML=`<div class="detail-head"><div><h2 id="detail-title">${escape(method)}</h2><p class="muted">${escape(row.family)} · ${escape(runtimeName(row.runtime))} · ${row.coverage} measured datasets</p></div><button id="close-detail" class="quiet-button">Close details</button></div><div class="detail-grid"><div><strong>${fmt(row.compression_rate,4)}</strong><span>average compression rate</span></div><div><strong>${fmt(row.compression_time,2)}</strong><span>encoding ns/point</span></div><div><strong>${fmt(row.decompression_time,2)}</strong><span>decoding ns/point</span></div><div><strong>${row.coverage} / ${row.totalDatasets}</strong><span>dataset coverage</span></div></div>${audit?`<div class="notice"><strong>Measurement audit · ${escape(audit.audited_at)}</strong>${[audit.implementation,audit.timing,audit.storage,audit.roundtrip,audit.comparison].map(n=>`<p>${escape(n)}</p>`).join('')}</div>`:''}${pipeline&&!audit?`<p class="source-code">Interpretive operator mapping: ${escape(typeof pipeline==='string'?pipeline:JSON.stringify(pipeline))}</p>`:''}<p class="muted">Dataset metrics below come from the published result files. Independent round-trip evidence and hardware are ${snapshot.benchmark?.roundtrip_evidence?'documented in the manifest':'not reported for this snapshot'}.</p><div class="detail-table"><table><thead><tr><th>Dataset</th><th>Configuration</th><th>Rate ↓</th><th>Encode ns/point ↓</th><th>Decode ns/point ↓</th><th>Original bytes</th><th>Compressed bytes</th><th>Dataset score ↓</th><th>Dataset Rank ↓</th><th>Rank candidates</th></tr></thead><tbody>${source.map(r=>`<tr><td>${escape(r.dataset)}</td><td>${escape(r.configuration)}</td><td class="numeric">${fmt(r.compression_rate,4)}</td><td class="numeric">${fmt(r.compression_time,2)}</td><td class="numeric">${fmt(r.decompression_time,2)}</td><td class="numeric">${fmt(r.original_bytes,0)}</td><td class="numeric">${fmt(r.compressed_bytes,0)}</td><td class="numeric">${fmt(r.dataset_score,4)}</td><td class="numeric">${fmt(r.dataset_rank,2)}</td><td class="numeric">${fmt(r.dataset_candidate_count,0)}</td></tr>`).join('')}</tbody></table></div>`;
110
  detail.hidden=false;$('close-detail').addEventListener('click',()=>{detail.hidden=true;selectedMethod=null;});
111
  if(focus){detail.scrollIntoView({block:'nearest',behavior:'auto'});detail.focus({preventScroll:true});}
112
  }
 
128
  }
129
  function csv() {
130
  const quote=v=>{let text=String(v??'');if(typeof v==='string'&&/^[=+@-]/.test(text))text="'"+text;return `"${text.replace(/"/g,'""')}"`;};
131
+ const weights=parseWeights($('weights').value);
132
+ const columns=['position','method','runtime','scope','row_mode','selected_dataset','sort_metric','sort_direction','average_compression_rate','overall_compression_rate','encode_ns_per_point','decode_ns_per_point','weight_a','weight_b','weight_c','normalized_encode','normalized_decode','composite_score','average_rank','rank_dataset_coverage','normalization_reference_rows','encode_min','encode_max','decode_min','decode_max','normalization','rank_definition','dataset_coverage','datasets_in_scope','selected_configuration_by_dataset'];
133
+ const body=currentRows.map((r,i)=>[i+1,r.method,r.runtime,scopeKey(),state().mode,state().dataset,sort.key,sort.direction===1?'ascending':'descending',r.compression_rate,r.overall_rate,r.compression_time,r.decompression_time,...weights.map(w=>w.toFixed(1)),r.normalized_compression_time,r.normalized_decompression_time,r.composite_score,r.average_rank,r.rank_coverage,r.normalization_reference_rows,r.encode_min,r.encode_max,r.decode_min,r.decode_max,'min-max over all measured rows in selected runtime/scope/row-mode before search and visibility filters','mean per-dataset composite-score fractional rank; per-dataset min-max times',r.coverage,r.totalDatasets,JSON.stringify(Object.fromEntries(r.source.map(s=>[s.dataset,s.configuration])))].map(quote).join(','));
134
  const url=URL.createObjectURL(new Blob(['\ufeff'+[columns.join(','),...body].join('\r\n')],{type:'text/csv;charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download=`THULab-compression-${state().runtime}-${scopeKey()}-${state().dataset}.csv`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
135
  }
136
  function renderAbout() {
137
  const b=snapshot.benchmark||{},s=snapshot.source||{},c=snapshot.coverage||{};
138
  const notes=[b.scope_note,b.aggregation_note,b.selection_note,...(b.notes||[]),...(b.measurement_boundaries||[])].filter(Boolean);
139
  $('methodology').innerHTML=notes.map(n=>`<p class="notice">${escape(n)}</p>`).join('');
140
+ for(const [method,audit] of Object.entries(b.method_audits||{}))$('methodology').innerHTML+=`<div class="notice"><strong>${escape(method)} measurement audit · ${escape(audit.audited_at)}</strong>${[audit.implementation,audit.timing,audit.storage,audit.roundtrip,audit.comparison].map(n=>`<p>${escape(n)}</p>`).join('')}</div>`;
141
  const definitions=b.metric_definitions||{};if(Object.keys(definitions).length)$('methodology').innerHTML+=`<dl class="definitions">${Object.entries(definitions).map(([k,v])=>`<div><dt>${escape(k.replaceAll('_',' '))}</dt><dd>${escape(typeof v==='string'?v:JSON.stringify(v))}</dd></div>`).join('')}</dl>`;
142
  const exclusions=c.exclusions||c.excluded_datasets||snapshot.exclusions||[];
143
  const info={ 'Snapshot exported':snapshot.exported_at||s.exported_at||'Not reported','Dataset scope generated':s.generated_at||'Not reported', 'Source checkout':s.git_commit||'Not reported','Source files':`${s.source_files?.length??0} metadata inputs and ${s.result_csv_files_used?.length??0} result CSVs (hashes in manifest)`,'Result entries across scopes':`${records.length} (scope slices can reuse a measurement)`,'Worktree source state':s.worktree_dirty_inputs?.length?'Includes current local changes; input file hashes identify the published snapshot.':'See manifest for source file hashes.','Runtime environment':b.hardware||'Hardware details are not recorded in this exported result snapshot.','Lossless evidence':b.roundtrip_evidence||'Lossless-design method filter from the source project; per-run round-trip verification is not recorded here.'};
 
158
  $('dataset-count').textContent=countFmt(new Set(records.map(r=>r.dataset)).size);$('method-count').textContent=countFmt(new Set(records.map(r=>r.family)).size);$('runtime-count').textContent=runtimes.length;$('record-count').textContent=countFmt(snapshot.coverage?.runtime_configuration_count??new Set(records.map(r=>`${r.runtime}\0${r.method}`)).size);
159
  const exported=snapshot.exported_at||snapshot.source?.exported_at||'not reported';$('snapshot').textContent=`Result snapshot exported · ${String(exported).replace('T',' ').slice(0,19)}${exported==='not reported'?'':' UTC'}`;$('footer-snapshot').textContent=`Source inputs documented in the manifest`;
160
  $('column-options').innerHTML=cols.map(c=>`<label><input type="checkbox" data-column="${c.key}" ${c.visible?'checked':''}>${escape(c.label)}</label>`).join('');
161
+ $('weights').innerHTML=WEIGHT_TENTHS.map(w=>`<option value="${w.join(',')}" ${w.join(',')==='4,3,3'?'selected':''}>a = ${(w[0]/10).toFixed(1)} · b = ${(w[1]/10).toFixed(1)} · c = ${(w[2]/10).toFixed(1)}</option>`).join('');
162
+ $('weights').addEventListener('change',refresh);
163
  for(const input of $('column-options').querySelectorAll('input'))input.addEventListener('change',()=>{cols.find(c=>c.key===input.dataset.column).visible=input.checked;renderTable();});
164
  for(const id of ['runtime','dtype','precision','dataset','family','row-mode','complete-only'])$(id).addEventListener('change',refresh);$('search').addEventListener('input',refresh);$('chart-metric').addEventListener('change',renderChart);$('download').addEventListener('click',csv);
165
+ $('reset').addEventListener('click',()=>{$('runtime').value=runtimes.includes('cpp')?'cpp':runtimes[0];$('dtype').value='overall';$('precision').value='all';$('dataset').value='all';$('row-mode').value='family';$('search').value='';$('family').value='all';$('weights').value='4,3,3';$('complete-only').checked=false;selectedMethod=null;$('method-detail').hidden=true;sort={key:'compression_rate',direction:1};setTab(activeTab);});
166
  for(const tab of document.querySelectorAll('[data-tab]'))tab.addEventListener('click',()=>{history.replaceState(null,'',`#${tab.dataset.tab}`);setTab(tab.dataset.tab);});
167
  const renderPaper=()=>{const rows=(snapshot.paper_table3?.rows||[]).filter(r=>r.scope===$('paper-scope').value);$('paper-table').querySelector('tbody').innerHTML=rows.map(r=>`<tr><td>${escape(r.method)}</td><td class="numeric">${fmt(r.rank,1)}</td><td class="numeric">${fmt(r.average_compression_rate,4)}</td><td class="numeric">${fmt(r.average_compression_time_ns_per_point,1)}</td><td class="numeric">${fmt(r.average_decompression_time_ns_per_point,1)}</td><td class="numeric">${fmt(r.median_balanced_score,4)}</td></tr>`).join('');};$('paper-scope').addEventListener('change',renderPaper);renderPaper();
168
  renderAbout();setTab(location.hash.slice(1)||'overall');
 
176
  $('theme').addEventListener('click',()=>{const next=document.documentElement.dataset.theme==='dark'?'light':'dark';document.documentElement.dataset.theme=next;$('theme').textContent=next==='dark'?'Light mode':'Dark mode';try{localStorage.setItem('thulab-theme',next);}catch{}});
177
  }
178
  setupTheme();
179
+ try { const response=await fetch('./ui-data.json?v=20261008-rank-v1',{cache:'no-cache'});if(!response.ok)throw new Error(`Snapshot request returned ${response.status}`);snapshot=await response.json();init(); }
180
  catch(e){$('load-error').hidden=false;$('load-error').textContent=`The result snapshot could not be loaded. Please reload the page or download the JSON from the repository. ${e.message}`;$('snapshot').textContent='Snapshot unavailable';$('leaderboard').querySelector('tbody').innerHTML='<tr><td class="empty-cell">Results unavailable.</td></tr>';}
data.json CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:b5230a0518ec3939048f0ce5731c2a29a661e98e3f493709c92fb260871b5e1c
3
- size 28047985
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:47ddcf459b1eedd8b1dcf89d07ee544e10b527e6da24c64f81de18628b324454
3
+ size 28048937
export_results.py CHANGED
@@ -462,12 +462,23 @@ def main() -> int:
462
  "benchmark": {
463
  "title": "THULab Time Series Compression Benchmark",
464
  "implementation_runtimes": list(RUNTIMES),
465
- "lossless_only": True,
466
- "current_result_selection": "all visible lossless algorithm configurations measured in the 19-dataset scope",
467
  "paper_result_selection": "the separate paper_table3 section contains its frozen 20-method C++ snapshot",
468
  "scope_note": "Current result rows cover numeric int/float report evidence for the retained 19 public datasets. They reflect the columns present in each report; they do not assert full-column coverage beyond those files.",
469
  "aggregation_note": "Average metrics are arithmetic means over available per-dataset measurements. Overall compression rate and weighted times use summed measured bytes, points, and nanoseconds only.",
470
- "selection_note": "Configuration views retain every visible measured lossless configuration. Family views choose the lowest-rate configuration independently for each dataset, scope, and runtime.",
 
 
 
 
 
 
 
 
 
 
 
471
  "notes": [
472
  "Missing combinations are unavailable rather than zero.",
473
  "Hardware identity and round-trip verification are not reported by these source CSVs.",
 
462
  "benchmark": {
463
  "title": "THULab Time Series Compression Benchmark",
464
  "implementation_runtimes": list(RUNTIMES),
465
+ "lossless_design_methods_only": True,
466
+ "current_result_selection": "visible configurations of methods categorized as lossless-design by the project; result-level float round trips are not established",
467
  "paper_result_selection": "the separate paper_table3 section contains its frozen 20-method C++ snapshot",
468
  "scope_note": "Current result rows cover numeric int/float report evidence for the retained 19 public datasets. They reflect the columns present in each report; they do not assert full-column coverage beyond those files.",
469
  "aggregation_note": "Average metrics are arithmetic means over available per-dataset measurements. Overall compression rate and weighted times use summed measured bytes, points, and nanoseconds only.",
470
+ "selection_note": "Configuration views retain measured configurations passing the project's method-name filter. This does not certify every result as lossless. Family views choose the lowest-rate configuration independently for each dataset, scope, and runtime.",
471
+ "method_audits": {
472
+ "DBP": {
473
+ "audited_at": "2026-10-08",
474
+ "label": "Kernel measurements",
475
+ "implementation": "Project DBP encodes deltas with ZigZag and ULEB128; it is not Parquet miniblock bit-packing.",
476
+ "timing": "Float preparation/scaling is outside the reported codec timer.",
477
+ "storage": "Reported float payload sizes omit the scale/mode metadata needed for complete reconstruction.",
478
+ "roundtrip": "Integer payload round-trip checks do not establish end-to-end bit-exact float reconstruction. Current Java/Python catalog scaling paths lack a bit-exact guard; current C++ has an exact-scaling check and IEEE-bit fallback, but old reports do not identify the exact executable revision.",
479
+ "comparison": "Compared methods may use different columns or point counts. Check the per-dataset original byte counts before comparing results.",
480
+ }
481
+ },
482
  "notes": [
483
  "Missing combinations are unavailable rather than zero.",
484
  "Hardware identity and round-trip verification are not reported by these source CSVs.",
index.html CHANGED
@@ -6,8 +6,8 @@
6
  <meta name="description" content="THULab time-series compression benchmark: measured compression rates, encoding and decoding costs, and dataset coverage.">
7
  <meta name="color-scheme" content="light dark">
8
  <title>THULab | Time Series Compression Benchmark</title>
9
- <link rel="stylesheet" href="style.css">
10
- <script src="app.js" type="module"></script>
11
  </head>
12
  <body>
13
  <a class="skip-link" href="#content">Skip to results</a>
@@ -52,6 +52,11 @@
52
  <div class="toolbar-actions"><button id="reset" class="quiet-button">Reset filters</button><button id="download" class="primary-button" disabled>Download CSV</button></div>
53
  </div>
54
  <div class="results-note" id="results-note">Compression rate = compressed bytes / original bytes. Lower rates and lower ns/point are better.</div>
 
 
 
 
 
55
  <div class="table-settings">
56
  <label><input id="complete-only" type="checkbox"> Only methods covering the full selection</label>
57
  <details><summary>Display columns</summary><div id="column-options" class="column-options"></div></details>
@@ -84,6 +89,8 @@
84
  <div><dt>Overall compression rate ↓</dt><dd>Total compressed bytes divided by total original bytes over the selected measured records.</dd></div>
85
  <div><dt>Encoding / decoding cost ↓</dt><dd>Nanoseconds per data point. The aggregation boundary is documented with the snapshot below.</dd></div>
86
  <div><dt>Coverage</dt><dd>Datasets with usable measurements for a method out of the datasets in the selected scope. Missing results are never filled with zero.</dd></div>
 
 
87
  </dl>
88
  <h3>Reading the rankings</h3>
89
  <p>Methods with different coverage are measured on different inputs. Coverage counts datasets, not identical columns or point counts. Use the full-coverage filter and inspect a single dataset's input sizes before drawing a direct comparison. Runtime filters keep Python, C++, and Java measurements separate.</p>
 
6
  <meta name="description" content="THULab time-series compression benchmark: measured compression rates, encoding and decoding costs, and dataset coverage.">
7
  <meta name="color-scheme" content="light dark">
8
  <title>THULab | Time Series Compression Benchmark</title>
9
+ <link rel="stylesheet" href="style.css?v=20261008-rank-v1">
10
+ <script src="app.js?v=20261008-rank-v1" type="module"></script>
11
  </head>
12
  <body>
13
  <a class="skip-link" href="#content">Skip to results</a>
 
52
  <div class="toolbar-actions"><button id="reset" class="quiet-button">Reset filters</button><button id="download" class="primary-button" disabled>Download CSV</button></div>
53
  </div>
54
  <div class="results-note" id="results-note">Compression rate = compressed bytes / original bytes. Lower rates and lower ns/point are better.</div>
55
+ <p id="audit-notice" class="audit-notice" hidden></p>
56
+ <div id="score-controls" class="score-controls">
57
+ <label>Score weights · a: rate / b: encode / c: decode<select id="weights" aria-label="Score weights a b c"></select></label>
58
+ <p id="score-note" class="muted" aria-live="polite">Positive weights in steps of 0.1, summing to 1.0.</p>
59
+ </div>
60
  <div class="table-settings">
61
  <label><input id="complete-only" type="checkbox"> Only methods covering the full selection</label>
62
  <details><summary>Display columns</summary><div id="column-options" class="column-options"></div></details>
 
89
  <div><dt>Overall compression rate ↓</dt><dd>Total compressed bytes divided by total original bytes over the selected measured records.</dd></div>
90
  <div><dt>Encoding / decoding cost ↓</dt><dd>Nanoseconds per data point. The aggregation boundary is documented with the snapshot below.</dd></div>
91
  <div><dt>Coverage</dt><dd>Datasets with usable measurements for a method out of the datasets in the selected scope. Missing results are never filled with zero.</dd></div>
92
+ <div><dt>Composite score ↓</dt><dd>a × average compression rate + b × normalized average encoding time + c × normalized average decoding time. Each time uses min–max over all measured rows in the selected runtime, scope and row mode, before search, family and coverage filters. If all times are equal, their normalized value is zero. All measured inputs must have usable timing metrics.</dd></div>
93
+ <div><dt>Average Rank ↓</dt><dd>Compute the same weighted score within each dataset, normalizing times within that dataset. Rank lower scores first; ties share their average position. Then take the arithmetic mean of the method's available dataset ranks. Weights are strictly positive tenths summing to 1.0. Missing measurements do not receive ranks.</dd></div>
94
  </dl>
95
  <h3>Reading the rankings</h3>
96
  <p>Methods with different coverage are measured on different inputs. Coverage counts datasets, not identical columns or point counts. Use the full-coverage filter and inspect a single dataset's input sizes before drawing a direct comparison. Runtime filters keep Python, C++, and Java measurements separate.</p>
manifest.json CHANGED
@@ -23,7 +23,7 @@
23
  "source_report_file_count": 114
24
  },
25
  "data_file": "data.json",
26
- "data_sha256": "b5230a0518ec3939048f0ce5731c2a29a661e98e3f493709c92fb260871b5e1c",
27
  "excluded_content": [
28
  "raw datasets",
29
  "user uploads",
 
23
  "source_report_file_count": 114
24
  },
25
  "data_file": "data.json",
26
+ "data_sha256": "47ddcf459b1eedd8b1dcf89d07ee544e10b527e6da24c64f81de18628b324454",
27
  "excluded_content": [
28
  "raw datasets",
29
  "user uploads",
ranking.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Ranking references include all measured methods in the selected runtime,
2
+ // scope and row mode. Search, family and coverage filters only hide rows.
3
+ export const WEIGHT_TENTHS = [];
4
+ for (let a=1;a<=8;a++) for (let b=1;b<=9-a;b++) WEIGHT_TENTHS.push([a,b,10-a-b]);
5
+
6
+ export function parseWeights(value) {
7
+ const parts=String(value).split(',').map(Number);
8
+ if(parts.length!==3||parts.some(n=>!Number.isInteger(n)||n<=0)||parts.reduce((s,n)=>s+n,0)!==10) throw new Error('Weights must be positive tenths summing to 1.0.');
9
+ return parts.map(n=>n/10);
10
+ }
11
+
12
+ const finite=v=>v!==null&&v!==undefined&&Number.isFinite(v);
13
+ const valid=r=>finite(r.compression_rate)&&r.compression_rate>0&&finite(r.compression_time)&&r.compression_time>=0&&finite(r.decompression_time)&&r.decompression_time>=0;
14
+ const average=values=>values.length?values.reduce((s,v)=>s+v,0)/values.length:null;
15
+
16
+ function bounds(rows,key) {
17
+ const values=rows.map(r=>r[key]).filter(finite);
18
+ return values.length?[Math.min(...values),Math.max(...values)]:null;
19
+ }
20
+
21
+ export function minMax(value,range) {
22
+ if(!finite(value)||!range)return null;
23
+ return range[0]===range[1]?0:(value-range[0])/(range[1]-range[0]);
24
+ }
25
+
26
+ function score(row,weights,encodeRange,decodeRange) {
27
+ const encode=minMax(row.compression_time,encodeRange),decode=minMax(row.decompression_time,decodeRange);
28
+ return {normalized_compression_time:encode,normalized_decompression_time:decode,composite_score:valid(row)&&encode!==null&&decode!==null?weights[0]*row.compression_rate+weights[1]*encode+weights[2]*decode:null};
29
+ }
30
+
31
+ export function addRankingMetrics(rows,weights) {
32
+ if(weights.length!==3||weights.some(w=>!finite(w)||w<=0)||Math.abs(weights.reduce((s,w)=>s+w,0)-1)>1e-12)throw new Error('Invalid scoring weights.');
33
+ const candidates=rows.filter(r=>valid(r)&&r.source.every(valid));
34
+ const encodeRange=bounds(candidates,'compression_time'),decodeRange=bounds(candidates,'decompression_time');
35
+ const byDataset=new Map(),ranks=new Map(),details=new Map();
36
+ for(const row of rows) {
37
+ ranks.set(row.method,[]);
38
+ for(const record of row.source.filter(valid)) {
39
+ if(!byDataset.has(record.dataset))byDataset.set(record.dataset,[]);
40
+ byDataset.get(record.dataset).push({...record,method:row.method});
41
+ }
42
+ }
43
+ for(const records of byDataset.values()) {
44
+ const encode=bounds(records,'compression_time'),decode=bounds(records,'decompression_time');
45
+ const ordered=records.map(r=>({...r,...score(r,weights,encode,decode)})).sort((a,b)=>a.composite_score-b.composite_score||a.method.localeCompare(b.method));
46
+ for(let start=0;start<ordered.length;) {
47
+ let end=start+1;
48
+ while(end<ordered.length&&ordered[end].composite_score===ordered[start].composite_score)end++;
49
+ const rank=(start+1+end)/2;
50
+ for(let i=start;i<end;i++) {
51
+ ranks.get(ordered[i].method).push(rank);
52
+ details.set(`${ordered[i].method}\0${ordered[i].dataset}`,{dataset_score:ordered[i].composite_score,dataset_rank:rank,dataset_candidate_count:ordered.length});
53
+ }
54
+ start=end;
55
+ }
56
+ }
57
+ return rows.map(row=>({...row,...(candidates.includes(row)?score(row,weights,encodeRange,decodeRange):{normalized_compression_time:null,normalized_decompression_time:null,composite_score:null}),average_rank:average(ranks.get(row.method)),rank_coverage:ranks.get(row.method).length,normalization_reference_rows:candidates.length,encode_min:encodeRange?.[0]??null,encode_max:encodeRange?.[1]??null,decode_min:decodeRange?.[0]??null,decode_max:decodeRange?.[1]??null,source:row.source.map(r=>({...r,...details.get(`${row.method}\0${r.dataset}`)}))}));
58
+ }
style.css CHANGED
@@ -7,3 +7,5 @@
7
  @media(max-width:768px){main{padding:0 16px}.site-header{padding:12px 16px;min-height:64px}.header-links{gap:10px;font-size:12px}.brand{gap:8px;font-size:13px}.brand-mark{width:34px;height:34px}.intro{padding:28px 0 20px}.intro-text{font-size:14px}.filters{grid-template-columns:repeat(2,minmax(0,1fr));padding:14px;gap:12px}.summary-strip{padding:14px 0}.summary-strip strong{font-size:21px}.summary-strip span{display:block;font-size:10px;line-height:1.4;padding:5px}.toolbar{align-items:start;flex-direction:column}.toolbar-actions{width:100%;justify-content:space-between}.table-settings{align-items:start;font-size:11px}.table-settings label{max-width:65%}.table-footer{flex-direction:column;gap:4px}.tab{padding:10px 14px;font-size:13px}.definitions,.method-groups{grid-template-columns:1fr}.detail-grid{grid-template-columns:repeat(2,1fr)}.method-detail{padding:16px}.chart-controls{flex-direction:column;align-items:start}.chart-controls label{width:100%}.chart{padding:6px}.summary-strip>div{min-width:0}footer{flex-direction:column;gap:4px}.table-wrap{max-height:550px}.header-links>a{display:none}h2{font-size:19px}}
8
  @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
9
  .intro{padding:16px 0 10px}.eyebrow{display:none}h1{font-size:clamp(24px,2.8vw,30px);margin-bottom:8px}.intro-text{font-size:13px}.snapshot{font-size:11px;margin-top:6px}.summary-strip{padding:10px 0}.summary-strip strong{font-size:20px}.filters{padding:12px;gap:8px}.toolbar{margin-top:12px;margin-bottom:4px}
 
 
 
7
  @media(max-width:768px){main{padding:0 16px}.site-header{padding:12px 16px;min-height:64px}.header-links{gap:10px;font-size:12px}.brand{gap:8px;font-size:13px}.brand-mark{width:34px;height:34px}.intro{padding:28px 0 20px}.intro-text{font-size:14px}.filters{grid-template-columns:repeat(2,minmax(0,1fr));padding:14px;gap:12px}.summary-strip{padding:14px 0}.summary-strip strong{font-size:21px}.summary-strip span{display:block;font-size:10px;line-height:1.4;padding:5px}.toolbar{align-items:start;flex-direction:column}.toolbar-actions{width:100%;justify-content:space-between}.table-settings{align-items:start;font-size:11px}.table-settings label{max-width:65%}.table-footer{flex-direction:column;gap:4px}.tab{padding:10px 14px;font-size:13px}.definitions,.method-groups{grid-template-columns:1fr}.detail-grid{grid-template-columns:repeat(2,1fr)}.method-detail{padding:16px}.chart-controls{flex-direction:column;align-items:start}.chart-controls label{width:100%}.chart{padding:6px}.summary-strip>div{min-width:0}footer{flex-direction:column;gap:4px}.table-wrap{max-height:550px}.header-links>a{display:none}h2{font-size:19px}}
8
  @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
9
  .intro{padding:16px 0 10px}.eyebrow{display:none}h1{font-size:clamp(24px,2.8vw,30px);margin-bottom:8px}.intro-text{font-size:13px}.snapshot{font-size:11px;margin-top:6px}.summary-strip{padding:10px 0}.summary-strip strong{font-size:20px}.filters{padding:12px;gap:8px}.toolbar{margin-top:12px;margin-bottom:4px}
10
+ .score-controls{display:flex;align-items:start;gap:20px;padding:12px 14px;margin:6px 0 12px;border:1px solid var(--line);border-radius:var(--radius);background:var(--soft)}.score-controls label{min-width:320px}.score-controls p{font-size:11px;line-height:1.7;margin:0;max-width:90ch}.score-controls select{height:35px}@media(max-width:768px){.score-controls{flex-direction:column;gap:10px}.score-controls label{min-width:0;width:100%}}
11
+ .audit-badge{display:block;color:var(--muted);font-size:10px;margin-top:4px}.audit-notice{font-size:12px;padding:10px 14px;margin:8px 0;border-left:3px solid var(--accent);background:var(--accent-soft);color:var(--text)}
ui-data.json CHANGED
The diff for this file is too large to render. See raw diff