Dashboard: scope donut + total + count to the active filter
Browse filesaggregateRows() recomputes a client-side aggregate over the filtered rows;
the donut, legend, center %, Team-total footer (relabeled 'Total (shown)'),
and the header count now reflect the current team/intern selection. Works in
both Files and Time units; unchanged when no filter is active.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -620,6 +620,27 @@ function rowBar(c, total) {
|
|
| 620 |
return `<div class="bar-track">${segs}</div>`;
|
| 621 |
}
|
| 622 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
function renderCharts(reports, agg) {
|
| 624 |
const el = document.getElementById("charts");
|
| 625 |
if (!reports.length) { el.innerHTML = ""; return; }
|
|
@@ -642,12 +663,10 @@ async function refresh() {
|
|
| 642 |
return;
|
| 643 |
}
|
| 644 |
const reports = sortReports(data.annotators || []);
|
| 645 |
-
const agg = data.aggregate || { total: 0, counts: {}, percent: 0 };
|
| 646 |
-
document.getElementById("count").textContent = reports.length;
|
| 647 |
-
|
| 648 |
-
renderCharts(reports, agg);
|
| 649 |
|
| 650 |
if (!reports.length) {
|
|
|
|
|
|
|
| 651 |
document.getElementById("table-wrap").innerHTML =
|
| 652 |
'<div class="empty">No reports yet. Annotators appear here after clicking “Share Progress”.</div>';
|
| 653 |
return;
|
|
@@ -660,12 +679,19 @@ async function refresh() {
|
|
| 660 |
const podium = sortDir === "desc" && PODIUM_KEYS.has(sortKey);
|
| 661 |
const MEDALS = ["gold", "silver", "bronze"];
|
| 662 |
|
| 663 |
-
//
|
|
|
|
| 664 |
let tableReports = reports;
|
| 665 |
if (internFilter === "only") tableReports = tableReports.filter(r => r.intern);
|
| 666 |
else if (internFilter === "exclude") tableReports = tableReports.filter(r => !r.intern);
|
| 667 |
if (teamFilter === "__unassigned__") tableReports = tableReports.filter(r => !TEAMS.has(r.team));
|
| 668 |
else if (teamFilter) tableReports = tableReports.filter(r => r.team === teamFilter);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
if (!tableReports.length) {
|
| 670 |
document.getElementById("table-wrap").innerHTML =
|
| 671 |
'<div class="empty">No annotators match the current filter.</div>';
|
|
@@ -710,14 +736,14 @@ async function refresh() {
|
|
| 710 |
</tr>`;
|
| 711 |
}).join("");
|
| 712 |
|
| 713 |
-
const ac = metric(
|
| 714 |
-
const at = metricTotal(
|
| 715 |
-
const ant = noTime(
|
| 716 |
const AF = (v) => ant ? "—" : fmt(v);
|
| 717 |
-
const ap = pct(ac, at); // unit-correct
|
| 718 |
const foot = `<tr>
|
| 719 |
<td class="rank"></td>
|
| 720 |
-
<td>Team total</td>
|
| 721 |
<td></td>
|
| 722 |
<td></td>
|
| 723 |
<td class="num">${AF(at)}</td>
|
|
|
|
| 620 |
return `<div class="bar-track">${segs}</div>`;
|
| 621 |
}
|
| 622 |
|
| 623 |
+
// Client-side aggregate over a set of grouped annotator rows — mirrors the
|
| 624 |
+
// server's _aggregate so the donut/legend/total can reflect the current filter.
|
| 625 |
+
// Equals the server "aggregate" when given the whole (unfiltered) list.
|
| 626 |
+
function aggregateRows(rows) {
|
| 627 |
+
const counts = {}, durations = {};
|
| 628 |
+
STATUS.forEach(k => { counts[k] = 0; durations[k] = 0; });
|
| 629 |
+
let total = 0, durationTotal = 0, durationKnown = 0;
|
| 630 |
+
rows.forEach(r => {
|
| 631 |
+
const c = r.counts || {}, d = r.durations || {};
|
| 632 |
+
STATUS.forEach(k => { counts[k] += c[k] || 0; durations[k] += d[k] || 0; });
|
| 633 |
+
total += r.total || 0;
|
| 634 |
+
durationTotal += r.durationTotal || 0;
|
| 635 |
+
durationKnown += r.durationKnown || 0;
|
| 636 |
+
});
|
| 637 |
+
const doneN = counts.delete + counts.correct + counts.edited;
|
| 638 |
+
return {
|
| 639 |
+
counts, durations, total, durationTotal, durationKnown,
|
| 640 |
+
done: doneN, percent: total ? Math.round(100 * doneN / total) : 0,
|
| 641 |
+
};
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
function renderCharts(reports, agg) {
|
| 645 |
const el = document.getElementById("charts");
|
| 646 |
if (!reports.length) { el.innerHTML = ""; return; }
|
|
|
|
| 663 |
return;
|
| 664 |
}
|
| 665 |
const reports = sortReports(data.annotators || []);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
|
| 667 |
if (!reports.length) {
|
| 668 |
+
renderCharts([], null);
|
| 669 |
+
document.getElementById("count").textContent = 0;
|
| 670 |
document.getElementById("table-wrap").innerHTML =
|
| 671 |
'<div class="empty">No reports yet. Annotators appear here after clicking “Share Progress”.</div>';
|
| 672 |
return;
|
|
|
|
| 679 |
const podium = sortDir === "desc" && PODIUM_KEYS.has(sortKey);
|
| 680 |
const MEDALS = ["gold", "silver", "bronze"];
|
| 681 |
|
| 682 |
+
// Apply the team/intern filters, then scope the donut, the Team-total row, and
|
| 683 |
+
// the header count to that selection (whole team when no filter is active).
|
| 684 |
let tableReports = reports;
|
| 685 |
if (internFilter === "only") tableReports = tableReports.filter(r => r.intern);
|
| 686 |
else if (internFilter === "exclude") tableReports = tableReports.filter(r => !r.intern);
|
| 687 |
if (teamFilter === "__unassigned__") tableReports = tableReports.filter(r => !TEAMS.has(r.team));
|
| 688 |
else if (teamFilter) tableReports = tableReports.filter(r => r.team === teamFilter);
|
| 689 |
+
const isFiltered = internFilter !== "" || teamFilter !== "";
|
| 690 |
+
|
| 691 |
+
const viewAgg = aggregateRows(tableReports);
|
| 692 |
+
document.getElementById("count").textContent = tableReports.length;
|
| 693 |
+
renderCharts(tableReports, viewAgg);
|
| 694 |
+
|
| 695 |
if (!tableReports.length) {
|
| 696 |
document.getElementById("table-wrap").innerHTML =
|
| 697 |
'<div class="empty">No annotators match the current filter.</div>';
|
|
|
|
| 736 |
</tr>`;
|
| 737 |
}).join("");
|
| 738 |
|
| 739 |
+
const ac = metric(viewAgg);
|
| 740 |
+
const at = metricTotal(viewAgg);
|
| 741 |
+
const ant = noTime(viewAgg);
|
| 742 |
const AF = (v) => ant ? "—" : fmt(v);
|
| 743 |
+
const ap = pct(ac, at); // unit-correct completion for the shown set
|
| 744 |
const foot = `<tr>
|
| 745 |
<td class="rank"></td>
|
| 746 |
+
<td>${isFiltered ? "Total (shown)" : "Team total"}</td>
|
| 747 |
<td></td>
|
| 748 |
<td></td>
|
| 749 |
<td class="num">${AF(at)}</td>
|