Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
426 changes: 410 additions & 16 deletions src/cloudai/report_generator/comparison_report.py

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions src/cloudai/report_generator/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ class TRGroupItem:

name: str
tr: TestRun
compact_name: str | None = None
full_name: str | None = None

@property
def v2_compact_name(self) -> str:
"""Return the concise label used by v2 reports."""
return self.compact_name or self.name

@property
def v2_full_name(self) -> str:
"""Return the detailed label used by v2 reports."""
return self.full_name or self.v2_compact_name


@dataclass
Expand Down
306 changes: 306 additions & 0 deletions src/cloudai/util/comparison-report-v2.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
{% extends "base-report.jinja2" %}

{% block extra_head %}
<meta charset="UTF-8">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<style>
.comparison-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.4rem;
padding: 1rem 1.1rem;
border: 1px solid var(--nv-border);
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 8px 24px rgba(17, 17, 17, 0.05);
}
.comparison-toolbar p {
margin: 0;
}
.label-toggle {
flex: 0 0 auto;
padding: 0.55rem 0.9rem;
border: 1px solid rgba(118, 185, 0, 0.32);
border-radius: 999px;
color: var(--nv-green-strong);
background: var(--nv-surface);
font-weight: 700;
cursor: pointer;
}
.label-toggle:hover {
background: var(--nv-green-soft);
}
.comparison-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.25rem;
}
.comparison-card {
min-width: 0;
padding: 1.25rem;
border: 1px solid var(--nv-border);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.98);
box-shadow: var(--nv-shadow);
}
.comparison-card h2 {
margin: 0;
font-size: 1.35rem;
}
.comparison-group {
margin: 0.15rem 0 1rem;
color: var(--nv-muted);
font-size: 0.92rem;
}
.chart-shell {
position: relative;
height: 340px;
min-height: 260px;
}
.chart-shell canvas {
width: 100% !important;
height: 100% !important;
}
.chart-fallback {
margin: 0.7rem 0 0;
font-size: 0.88rem;
}
.chart-shell.is-enhanced + .chart-fallback {
display: none;
}
.overlap-note {
margin: 0.65rem 0 0;
padding: 0.65rem 0.8rem;
border-radius: var(--radius-sm);
color: var(--nv-slate);
background: var(--nv-green-soft);
font-size: 0.88rem;
font-weight: 600;
}
.comparison-table-shell {
overflow-x: auto;
margin-top: 1.1rem;
border: 1px solid rgba(17, 17, 17, 0.07);
border-radius: var(--radius-md);
}
.comparison-table {
margin: 0;
min-width: 680px;
border-radius: 0;
box-shadow: none;
font-size: 0.9rem;
}
.comparison-table th {
vertical-align: bottom;
}
.run-label {
display: block;
max-width: 28rem;
overflow-wrap: anywhere;
color: var(--nv-ink);
}
.metric-label {
display: block;
margin-top: 0.2rem;
color: var(--nv-muted);
font-size: 0.76rem;
font-weight: 600;
}
.empty-report {
padding: 1rem;
border: 1px solid var(--nv-border);
border-radius: var(--radius-md);
background: var(--nv-surface);
}
@media (max-width: 1000px) {
.comparison-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.comparison-toolbar {
align-items: flex-start;
flex-direction: column;
}
.comparison-card {
padding: 1rem;
}
.chart-shell {
height: 300px;
}
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
const charts = [];

document.querySelectorAll(".js-comparison-chart").forEach((canvas) => {
const dataNode = document.getElementById(canvas.dataset.chartDataId);
if (!dataNode || typeof Chart === "undefined") {
return;
}

const payload = JSON.parse(dataNode.textContent);
const isLine = payload.type === "line";
const chart = new Chart(canvas, {
type: payload.type,
data: {
labels: payload.labels,
datasets: payload.datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: "index",
intersect: false
},
plugins: {
legend: {
position: "bottom",
labels: {
usePointStyle: true,
boxWidth: 10,
padding: 16
}
},
tooltip: {
callbacks: {
title: function (items) {
if (!items.length) {
return "";
}
const parsed = items[0].parsed;
return payload.x_axis_label + ": " + (isLine ? parsed.x : items[0].label);
},
label: function (context) {
const value = context.parsed.y;
return context.dataset.label + " — " + payload.y_axis_label + ": " + value;
}
}
}
},
scales: {
x: {
type: isLine ? "logarithmic" : "category",
title: {
display: true,
text: payload.x_axis_label
},
grid: {
color: "rgba(57, 66, 78, 0.08)"
}
},
y: {
beginAtZero: payload.type === "bar",
title: {
display: true,
text: payload.y_axis_label
},
grid: {
color: "rgba(57, 66, 78, 0.10)"
}
}
}
}
});
charts.push(chart);
canvas.parentElement.classList.add("is-enhanced");
});

const toggle = document.querySelector(".js-label-toggle");
if (!toggle) {
return;
}

let showFullLabels = false;
toggle.addEventListener("click", function () {
showFullLabels = !showFullLabels;
toggle.setAttribute("aria-pressed", showFullLabels ? "true" : "false");
toggle.textContent = showFullLabels ? "Show compact labels" : "Show full labels";

document.querySelectorAll(".js-switchable-label").forEach((label) => {
label.textContent = showFullLabels ? label.dataset.full : label.dataset.compact;
});

charts.forEach((chart) => {
chart.data.datasets.forEach((dataset) => {
dataset.label = showFullLabels ? dataset.fullLabel : dataset.compactLabel;
});
chart.update();
});
});
});
</script>
{% endblock %}

{% block content %}
{% if sections %}
<div class="comparison-toolbar">
<p>Compact case labels are shown by default. Full labels expose only parameters that differ.</p>
<button type="button" class="label-toggle js-label-toggle" aria-pressed="false">Show full labels</button>
</div>

<div class="comparison-grid">
{% for section in sections %}
<section class="comparison-card">
<h2>{{ section.title }}</h2>
<p class="comparison-group">{{ section.group_name }}</p>

<div class="chart-shell">
<canvas
class="js-comparison-chart"
data-chart-data-id="{{ section.chart.id }}-data"
aria-label="{{ section.title }} comparison chart"
></canvas>
</div>
<p class="chart-fallback">Interactive chart unavailable. The complete numeric data remains available below.</p>
<noscript><p class="chart-fallback">JavaScript is disabled. The complete numeric data remains available below.</p></noscript>
<script type="application/json" id="{{ section.chart.id }}-data">{{ section.chart | tojson }}</script>

{% for overlap in section.chart.overlaps %}
<p class="overlap-note">{{ overlap }}</p>
{% endfor %}

<div class="comparison-table-shell">
<table class="comparison-table">
<thead>
<tr>
{% for header in section.table.info_headers %}
<th>{{ header }}</th>
{% endfor %}
{% for header in section.table.data_headers %}
<th>
<span
class="run-label js-switchable-label"
data-compact="{{ header.compact_name }}"
data-full="{{ header.full_name }}"
>{{ header.compact_name }}</span>
<span class="metric-label">{{ header.metric }}</span>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in section.table.rows %}
<tr>
{% for value in row.info_cells %}
<td>{{ value }}</td>
{% endfor %}
{% for value in row.data_cells %}
<td>{{ value }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endfor %}
</div>
{% else %}
<p class="empty-report">No comparison data found.</p>
{% endif %}
{% endblock %}
Loading
Loading