README / benchmark-chart.js
Kearm's picture
Upload benchmark-chart.js with huggingface_hub
f878a0f verified
Raw
History Blame Contribute Delete
5.49 kB
class ResmpBenchmarkChart extends HTMLElement {
connectedCallback() {
const source = this.querySelector('script[type="application/json"]');
if (source) this._config = JSON.parse(source.textContent);
this.render();
}
set config(value) { this._config = value; this.render(); }
get config() { return this._config; }
render() {
const cfg = this._config;
if (!cfg) return;
this.validate(cfg);
const all = cfg.groups.flatMap(g => g.panels.flatMap(p => Object.values(p.values))).filter(Number.isFinite);
const min = Math.min(0, ...all), max = Math.max(1, ...all);
this.innerHTML = `<section class="resmp-chart">
<header class="chart-head">
<div class="chart-nameboard"><span><small>RESMP.DEV / EVALUATION</small>${this.escape(cfg.title)}</span></div>
<div class="chart-actions"><button type="button" data-export>EXPORT SVG</button><button type="button" data-print>PRINT</button></div>
</header>
<p class="chart-subtitle">${this.escape(cfg.subtitle || '')}</p>
<div class="chart-legend">${cfg.series.map(s => `<span><i style="--series:${this.escape(s.color)}"></i>${this.escape(s.label)}</span>`).join('')}</div>
<div class="chart-groups">${cfg.groups.map((g, gi) => `<section class="chart-group">
<header><span>${String(gi + 1).padStart(2,'0')}</span><h2>${this.escape(g.name)}</h2><small>${cfg.higherIsBetter === false ? 'LOWER IS BETTER' : 'HIGHER IS BETTER'}</small></header>
<div class="chart-panels">${g.panels.map(p => this.panel(p, cfg.series, min, max)).join('')}</div>
</section>`).join('')}</div>
<footer class="chart-footer"><span>RESMP BENCHMARK INSTRUMENT / REV 01</span><span>DIRECT LABELS · FIXED SERIES ORDER · SHARED SCALE</span></footer>
</section>`;
this.querySelector('[data-export]').addEventListener('click', () => this.exportSvg());
this.querySelector('[data-print]').addEventListener('click', () => window.print());
}
panel(panel, series, min, max) {
const width = 300, height = 178, left = 18, right = 12, top = 22, bottom = 34;
const plotH = height - top - bottom, zeroY = top + (max / (max - min)) * plotH;
const gap = 7, barW = Math.min(36, (width-left-right-gap*(series.length-1))/series.length);
const total = series.length * barW + (series.length - 1) * gap, start = (width - total) / 2;
const bars = series.map((s, i) => {
const raw = panel.values[s.id];
if (raw === null || raw === undefined || !Number.isFinite(Number(raw))) {
const x = start + i * (barW + gap);
return `<g><rect x="${x}" y="${top}" width="${barW}" height="${plotH}" fill="none" stroke="${this.escape(s.color)}" stroke-dasharray="3 3" opacity=".35"/><text x="${x+barW/2}" y="${zeroY-6}" text-anchor="middle" class="value">N/A</text></g>`;
}
const v = Number(raw), yv = top + ((max - v) / (max - min)) * plotH;
const y = Math.min(yv, zeroY), h = Math.max(1, Math.abs(zeroY - yv)), x = start + i * (barW + gap);
const labelY = v >= 0 ? y - 5 : y + h + 11;
return `<g><rect x="${x}" y="${y}" width="${barW}" height="${h}" fill="${this.escape(s.color)}"/><text x="${x+barW/2}" y="${labelY}" text-anchor="middle" class="value">${v.toFixed(2)}</text><text x="${x+barW/2}" y="${Math.min(y+h-7, zeroY-7)}" text-anchor="middle" class="series-code">${this.escape(s.id.slice(0,2).toUpperCase())}</text></g>`;
}).join('');
return `<figure><svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${this.escape(panel.name)} benchmark"><line x1="${left}" y1="${zeroY}" x2="${width-right}" y2="${zeroY}" class="baseline"/>${bars}<text x="${width/2}" y="${height-8}" text-anchor="middle" class="panel-name">${this.escape(panel.name)}</text></svg></figure>`;
}
validate(cfg) {
if (!cfg || typeof cfg !== 'object') throw new TypeError('Benchmark config must be an object.');
if (!Array.isArray(cfg.series) || cfg.series.length < 1) throw new TypeError('Benchmark config requires at least one series.');
if (!Array.isArray(cfg.groups) || cfg.groups.length < 1) throw new TypeError('Benchmark config requires at least one group.');
const ids = cfg.series.map(s => s.id);
if (new Set(ids).size !== ids.length) throw new TypeError('Series IDs must be unique.');
for (const group of cfg.groups) {
if (!Array.isArray(group.panels) || group.panels.length < 1) throw new TypeError(`Group ${group.name || '(unnamed)'} requires panels.`);
}
}
exportSvg() {
const chart = this.querySelector('.resmp-chart').cloneNode(true); chart.querySelector('.chart-actions')?.remove();
const css = [...document.styleSheets].flatMap(s => { try { return [...s.cssRules].map(r=>r.cssText) } catch { return [] } }).join('\n');
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="${Math.max(900, this.querySelector('.resmp-chart').scrollHeight)}"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml"><style>${css}</style>${chart.outerHTML}</div></foreignObject></svg>`;
const url = URL.createObjectURL(new Blob([svg], {type:'image/svg+xml'}));
const a = document.createElement('a'); a.href = url; a.download = 'resmp-benchmark.svg'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 500);
}
escape(value) { return String(value).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
}
customElements.define('resmp-benchmark-chart', ResmpBenchmarkChart);