/* Additional chart views for the existing PaddyLiteX benchmark interface. */ (function () { 'use strict'; const metricNames = { precision: 'Precision', recall: 'Recall', map50: 'mAP@0.5', map5095: 'mAP@0.5:0.95', }; const hiddenSeries = new Map(); const $ = (id) => document.getElementById(id); const escape = (value) => String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[character])); const hasScore = (value) => typeof value === 'number' && Number.isFinite(value); const percent = (value) => hasScore(value) ? `${value.toFixed(2)}%` : 'N/A'; const classLabel = (name) => name.replace(/^Rice__/, '').replace(/([a-z])([A-Z])/g, '$1 $2'); const modelName = (model) => model.label.replace(/\bDenseNet121\b/g, 'Yolov8n_DenseNet121'); function legend(models, key, redraw) { if (!hiddenSeries.has(key)) hiddenSeries.set(key, new Set()); const hidden = hiddenSeries.get(key); const markup = models.map((model) => ` `).join(''); return { hidden, markup, bind(container) { container.querySelectorAll('[data-series]').forEach((button) => { button.addEventListener('click', () => { const id = button.dataset.series; if (hidden.has(id)) hidden.delete(id); else hidden.add(id); redraw(); }); }); } }; } function radar(axes, models, getScore, key, redraw) { const width = 900; const height = 590; const center = { x: 450, y: 285 }; const radius = 195; const point = (index, score, padding = 0) => { const angle = -Math.PI / 2 + index * 2 * Math.PI / axes.length; const distance = radius * score / 100 + padding; return { x: center.x + Math.cos(angle) * distance, y: center.y + Math.sin(angle) * distance }; }; const points = (score) => axes.map((_, index) => { const p = point(index, score); return `${p.x},${p.y}`; }).join(' '); const controls = legend(models, key, redraw); const rings = [20, 40, 60, 80, 100].map((score) => ` ${score}%`).join(''); const spokes = axes.map((axis, index) => { const edge = point(index, 100); const label = point(index, 100, 29); const anchor = Math.abs(label.x - center.x) < 15 ? 'middle' : label.x < center.x ? 'end' : 'start'; const words = axis.label.split(' '); const lines = words.length > 3 ? [words.slice(0, Math.ceil(words.length / 2)).join(' '), words.slice(Math.ceil(words.length / 2)).join(' ')] : [axis.label]; return ` ${lines.map((line, i) => `${i ? ' ' : ''}${escape(line)}`).join('')} `; }).join(''); let incomplete = 0; const polygons = models.map((model, modelIndex) => { if (controls.hidden.has(model.id)) return ''; const scores = axes.map((axis) => getScore(model, axis)); if (!scores.every(hasScore)) { incomplete += 1; return ''; } const vertices = scores.map((score, i) => point(i, score)); const dash = ['', '7 3', '2 3', '10 3 2 3'][modelIndex % 4]; const description = `${modelName(model)}: ${axes.map((axis, i) => `${axis.label} ${percent(scores[i])}`).join('; ')}`; return ` ${escape(description)} ${vertices.map((p, i) => `${escape(modelName(model))} · ${escape(axes[i].label)}: ${percent(scores[i])}`).join('')}`; }).join(''); return { controls, markup: `
${rings}${spokes}${polygons}
${controls.markup}

Select model names to show or hide their series. All axes use 0–100%.${incomplete ? ' Models with missing scores are omitted from the radar.' : ''}

` }; } function scoreColor(value) { if (!hasScore(value)) return '#edf1f5'; // A fixed 0–100% scale keeps heatmaps comparable across metrics and studies. const ratio = Math.max(0, Math.min(100, value)) / 100; const start = [245, 248, 252]; const end = [27, 113, 143]; return `rgb(${start.map((channel, i) => Math.round(channel + (end[i] - channel) * ratio)).join(',')})`; } function heatmap(rowLabels, columnLabels, getScore, caption) { return `
${columnLabels.map((column) => ``).join('')} ${rowLabels.items.map((row) => ` ${columnLabels.map((column) => { const value = getScore(row, column); return ``; }).join('')} `).join('')}
${escape(caption)}
${escape(rowLabels.heading)}${escape(column.label).replace(/_/g, '_')}
${escape(row.label)}${percent(value)}
0%100%
`; } function dotRows(models, getScore, format, limit) { return models.map((model) => { const value = getScore(model); const label = hasScore(value) ? format(value) : 'N/A'; const position = hasScore(value) ? Math.max(0, Math.min(100, value / limit * 100)) : 0; return `
${escape(modelName(model))}${label}
`; }).join(''); } function renderComparison(containerId, models, metric, options) { const container = $(containerId); const redraw = () => renderComparison(containerId, models, metric, options); if (options.type === 'radar') { const axes = Object.entries(metricNames).map(([id, label]) => ({ id, label })); const chart = radar(axes, models, (model, axis) => model[axis.id], `quality-${options.study}`, redraw); container.innerHTML = chart.markup; chart.controls.bind(container); } else if (options.type === 'heatmap') { container.innerHTML = heatmap( { heading: 'Model', items: models.map((model) => ({ id: model.id, label: modelName(model), model })) }, Object.entries(metricNames).map(([id, label]) => ({ id, label })), (row, column) => row.model[column.id], 'Detection performance across all four metrics (%)', ); } else { container.innerHTML = dotRows(models, (model) => model[metric], options.format, options.limit) + `
0${options.format(options.limit)}
`; } } function renderClasses(models, rows, metric, options) { const container = $('class-chart'); const getScore = (row, model) => row.models[model.id]?.[metric]; if (options.type === 'radar') { // Keep class axes in dataset order, even if bar/heatmap sorting was changed. const axes = [...rows].sort((a, b) => a.id - b.id) .map((row) => ({ id: row.id, label: `${row.id} · ${classLabel(row.name)}`, row })); const redraw = () => renderClasses(models, rows, metric, options); const chart = radar(axes, models, (model, axis) => getScore(axis.row, model), `classes-${options.study}`, redraw); container.innerHTML = chart.markup; chart.controls.bind(container); } else if (options.type === 'heatmap') { container.innerHTML = heatmap( { heading: 'Disease class', items: rows.map((row) => ({ label: `${row.id} · ${row.name}`, row })) }, models.map((model) => ({ label: modelName(model), model })), (row, column) => getScore(row.row, column.model), `${options.label} by disease class (%)`, ); } else { container.innerHTML = rows.map((row) => `

${row.id} · ${escape(row.name)}

${dotRows(models, (model) => getScore(row, model), percent, 100)}
`).join('') + '
0%100%
'; } } window.PaddyLiteXCharts = { renderComparison, renderClasses }; })();