shaanilmo Claude Opus 4.8 commited on
Commit
cce29f4
·
1 Parent(s): 4881fdf

Dashboard: scope donut + total + count to the active filter

Browse files

aggregateRows() 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>

Files changed (1) hide show
  1. app.py +36 -10
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
- // Filters apply to the table rows only (charts + team total stay whole-team).
 
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(agg);
714
- const at = metricTotal(agg);
715
- const ant = noTime(agg);
716
  const AF = (v) => ant ? "—" : fmt(v);
717
- const ap = pct(ac, at); // unit-correct team completion (= agg.percent in files mode)
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>