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 = ` RESMP.DEV / EVALUATION${this.escape(cfg.title)} EXPORT SVGPRINT ${this.escape(cfg.subtitle || '')} ${cfg.series.map(s => `${this.escape(s.label)}`).join('')} ${cfg.groups.map((g, gi) => ` ${String(gi + 1).padStart(2,'0')}${this.escape(g.name)}${cfg.higherIsBetter === false ? 'LOWER IS BETTER' : 'HIGHER IS BETTER'} ${g.panels.map(p => this.panel(p, cfg.series, min, max)).join('')} `).join('')} `; 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 `N/A`; } 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 `${v.toFixed(2)}${this.escape(s.id.slice(0,2).toUpperCase())}`; }).join(''); return `${bars}${this.escape(panel.name)}`; } 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 = `${chart.outerHTML}`; 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } } customElements.define('resmp-benchmark-chart', ResmpBenchmarkChart);
${this.escape(cfg.subtitle || '')}