/* 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: `
${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 `
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}
${hasScore(value) ? `` : ''}
`;
}).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 };
})();