Skip to content

Commit 85db82a

Browse files
authored
Merge pull request #22 from nyman-i/dev
Per-day graphs, CCT streak line, RRT global timer seconds
2 parents 6f894d8 + 59cd8f7 commit 85db82a

8 files changed

Lines changed: 87 additions & 31 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ by default; multiplication, subtraction and difference are also available).
6060
The pace is adaptive: answer streaks speed the interval up, miss streaks
6161
slow it back down, clamped between a configurable floor and ceiling. Each
6262
session starts at the pace the last one ended on, so you pick up where you
63-
left off. The in-game HUD shows time (or answers) remaining, your streak
64-
and the current pace with its trend, each answer gets an instant ✓/✗
63+
left off. The in-game HUD shows time (or answers) remaining and the current
64+
pace with its trend, a small green "N in a row" under the digit tracks your
65+
streak (switchable off), each answer gets an instant ✓/✗
6566
verdict line, and a session ends on a summary card - accuracy, best
6667
streak, response times and how far the pace moved. The screen stays awake
6768
during a session. Choose

cct.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@
8080
<select-row select-id="cct-inputmethod" label="Input method"
8181
options="physical=Physical keyboard|keypad=On-screen keypad"
8282
tooltip="Type answers on|your keyboard, or|tap them on big|on-screen buttons." tooltip-side="right"></select-row>
83+
<switch-row input-id="cct-showstreak" label="Show streak"
84+
tooltip="Small green|&quot;N in a row&quot;|under the digit." tooltip-side="right"></switch-row>
8385
</details>
8486
<details class="panel-section">
8587
<summary class="panel-heading">Reset</summary>
@@ -165,6 +167,8 @@
165167
<div id="cct-keypad" class="cct-keypad" hidden></div>
166168
<div class="cct-result" id="cct-result" hidden></div>
167169
</div>
170+
<!-- subtle "N in a row" under the frame, same treatment as RRT's -->
171+
<div id="cct-streak" class="progress-tracker"></div>
168172
<div class="cct-actions">
169173
<button type="button" id="cct-start" class="nback__start">START</button>
170174
<button type="button" id="cct-pause" class="nback__start" hidden>PAUSE</button>

js/cct/graphs.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,25 +58,27 @@ class CctGraphs extends CvMetricGraphs {
5858
has: s => s.fastestResponseTimeMs != null,
5959
axis: { title: { display: true, text: 'fastest response time (ms)' } },
6060
fmt: v => `${Math.round(v)} ms`,
61+
agg: 'min',
6162
},
6263
streak: {
6364
y: s => s.bestStreak,
6465
has: s => s.bestStreak != null,
6566
axis: { min: 0, title: { display: true, text: 'best streak' }, ticks: { precision: 0 } },
6667
fmt: v => `${v}`,
68+
agg: 'max',
6769
},
6870
interval: {
6971
y: s => s.lowestIntervalMs / 1000,
7072
has: s => s.lowestIntervalMs != null,
7173
axis: { min: 0, title: { display: true, text: 'lowest interval reached (s)' } },
7274
fmt: v => `${v.toFixed(1)} s`,
75+
agg: 'min',
7376
},
7477
};
7578
}
7679

7780
includes(s) { return s.status === 'Completed' && s.totalQuestionsAsked > 0; }
7881
groupKey(s) { return window.cvCctDisplay.variant(s); }
79-
pointMeta(s) { return { minutes: Math.round((s.durationMs ?? 0) / 60000) }; }
80-
tooltipSession(r) { return r.minutes > 0 ? ` (${r.minutes} min)` : ''; }
82+
minutes(s) { return (s.durationMs ?? 0) / 60000; }
8183
}
8284
customElements.define('cct-graphs', CctGraphs);

js/cct/page.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const keySettingMap = {
3535
'cct-beepenabled': 'beepEnabled',
3636
'cct-presentation': 'presentationMode',
3737
'cct-inputmethod': 'inputMethod',
38+
'cct-showstreak': 'showStreak',
3839
'cct-dailygoal': 'dailyProgressGoal',
3940
'cct-weeklygoal': 'weeklyProgressGoal',
4041
}
@@ -138,7 +139,8 @@ async function renderGoalTrackers() {
138139
}
139140
}
140141

141-
subscribe(() => { populateSettings(); renderGoalTrackers() })
142+
// renderStreak too, so toggling "Show streak" mid-session lands right away
143+
subscribe(() => { populateSettings(); renderGoalTrackers(); renderStreak(stats.streak) })
142144

143145
$('cct-reset-settings').addEventListener('click', () => {
144146
resetSettings()
@@ -181,9 +183,18 @@ function renderHud() {
181183
parts.push(`${stats.correctAnswers}/${hudGoal.targetCorrect} target`)
182184
}
183185
parts.push(`Correct ${stats.correctAnswers}/${stats.totalQuestions}`)
184-
parts.push(`Streak ${stats.streak}`)
185186
parts.push(`Interval ${stats.interval}ms${intervalTrend}`)
186187
$('cct-hud').innerHTML = parts.map(p => `<div>${p}</div>`).join('')
188+
renderStreak(stats.streak)
189+
}
190+
191+
// streak lives under the frame instead of in the HUD - same subtle
192+
// "N in a row", hidden at 0, as RRT's progress tracker
193+
function renderStreak(streak) {
194+
const show = streak > 0 && getSettings().showStreak
195+
const el = $('cct-streak')
196+
el.classList.toggle('visible', show)
197+
el.textContent = show ? `${streak} in a row` : ''
187198
}
188199

189200
// same green/amber flash N-Back's HUD gives on auto-progression
@@ -311,6 +322,7 @@ function endUi(record) {
311322
$('cct-keypad').hidden = true
312323
$('cct-answer').hidden = true
313324
$('cct-verdict').hidden = true
325+
renderStreak(0)
314326
$('cct-start').textContent = 'START'
315327
$('cct-pause').hidden = true
316328
clearInterval(hudTimer)

js/cct/settings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const defaultSettings = {
2121
targetCorrect: 500,
2222
arithmeticMode: 'addition', // 'addition' | 'multiplication' | 'subtraction' | 'difference'
2323
presentationMode: 'audiovisual', // 'audiovisual' | 'audio' | 'visual'
24+
showStreak: true, // the "N in a row" line under the frame
2425
// 'physical' | 'keypad' - touch devices default to the on-screen keypad
2526
// so the OS keyboard never pops up over the play area
2627
inputMethod: (typeof matchMedia !== 'undefined' && matchMedia('(pointer: coarse)').matches)

js/components/metric-graphs.js

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,51 @@
99
// it renders all four of its charts at once from day-bucketed data, a
1010
// different shape than the per-record metric charts here.
1111
//
12+
// Points are one per day per variant, not one per session (RRT's shape):
13+
// a dozen short games in an evening read as one day's result instead of a
14+
// dozen dots. Metrics say how their day collapses via `agg`.
15+
//
1216
// Subclass contract:
1317
// views() -> [{ view, label }] - first entry is the default tab;
1418
// a 'time' view gets the minutes-per-day bar chart
15-
// metrics() -> { view: { y, has, axis, fmt, empty? } }
19+
// metrics() -> { view: { y, has, axis, fmt, empty?, agg? } }
20+
// agg: 'mean' (default) | 'min' | 'max' - how a day's
21+
// sessions collapse to one point ('min' for fastest-X,
22+
// 'max' for best-X, mean for everything else)
1623
// includes(r) -> record filter (completed-status check)
1724
// groupKey(r) -> dataset label (structural-variant grouping)
18-
// pointMeta(r) -> extra fields carried on each point for the tooltip
19-
// tooltipSession(raw) -> session summary appended to the dataset label,
20-
// e.g. " 3-back (5 min)" - tooltips show the training
21-
// mode + value only; other metrics have their own graphs
25+
// minutes(r) -> that record's play time in minutes, summed per day
26+
// for the tooltip
2227
// emptyDefault -> getter, default empty-state text
28+
29+
// 4 AM day rollover, so a late-night session counts toward the day it started
30+
// in. Deliberately the same shape as getTruncatedDate (js/quadbox/engine/utils.js)
31+
// and dayKey (js/cct/engine/gamedb.js) - roll the *local* hour back rather than
32+
// subtracting 4h of absolute time, or a DST-shift day would bucket a session
33+
// differently here than in the minutes-per-day bar chart those two feed.
34+
// Duplicated rather than imported: both of those are ES modules and this file
35+
// is a classic script.
36+
const cvDayKey = (timestamp) => {
37+
const d = new Date(timestamp);
38+
if (d.getHours() < 4) d.setDate(d.getDate() - 1);
39+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
40+
};
41+
42+
// ponytail: mean is unweighted - a 3-trial warmup counts as much as a 60-trial
43+
// run. Weight by trial count if that skews days in practice.
44+
const cvCollapse = {
45+
mean: ys => ys.reduce((a, b) => a + b, 0) / ys.length,
46+
min: ys => Math.min(...ys),
47+
max: ys => Math.max(...ys),
48+
};
49+
50+
// sessions/minutes count only the day's records that HAVE this metric, so a
51+
// reaction-time tab can legitimately read fewer sessions than the accuracy tab
52+
const cvDaySummary = ({ sessions, minutes }) => {
53+
const mins = Math.round(minutes);
54+
return ` · ${sessions} session${sessions === 1 ? '' : 's'}${mins > 0 ? `, ${mins} min` : ''}`;
55+
};
56+
2357
class CvMetricGraphs extends HTMLElement {
2458
connectedCallback() {
2559
const views = this.views();
@@ -84,19 +118,28 @@ class CvMetricGraphs extends HTMLElement {
84118
else this.renderMetric(this.metrics()[this.view]);
85119
}
86120

87-
renderMetric({ y, has, axis, fmt, empty }) {
121+
renderMetric({ y, has, axis, fmt, empty, agg }) {
88122
const rows = this.records
89123
.filter(r => this.includes(r) && has(r))
90124
.sort((a, b) => a.timestamp - b.timestamp);
125+
// variant -> day -> the day's sessions; rows are timestamp-sorted, so
126+
// insertion order already puts each dataset's days in ascending order
91127
const groups = {};
92128
for (const r of rows) {
93129
const key = this.groupKey(r);
94-
groups[key] = groups[key] ?? [];
95-
groups[key].push({ x: r.timestamp, y: y(r), ...this.pointMeta(r) });
130+
const day = cvDayKey(r.timestamp);
131+
groups[key] = groups[key] ?? {};
132+
const bucket = groups[key][day] = groups[key][day] ?? { x: day, ys: [], sessions: 0, minutes: 0 };
133+
bucket.ys.push(y(r));
134+
bucket.sessions += 1;
135+
bucket.minutes += this.minutes(r);
96136
}
137+
const collapse = cvCollapse[agg ?? 'mean'];
97138
const { fg, palette } = this.tokens();
98-
const datasets = Object.entries(groups).map(([label, data], i) => ({
99-
label, data, borderColor: this.paletteColor(i, palette),
139+
const datasets = Object.entries(groups).map(([label, days], i) => ({
140+
label,
141+
data: Object.values(days).map(d => ({ ...d, y: collapse(d.ys) })),
142+
borderColor: this.paletteColor(i, palette),
100143
backgroundColor: this.paletteColor(i, palette), tension: 0.2, pointRadius: 3,
101144
}));
102145
this.setEmpty(datasets.some(d => d.data.length > 0), empty);
@@ -116,7 +159,7 @@ class CvMetricGraphs extends HTMLElement {
116159
legend: { labels: { color: fg } },
117160
tooltip: {
118161
callbacks: {
119-
label: (item) => `${item.dataset.label}${this.tooltipSession(item.raw)}: ${fmt(item.raw.y)}`,
162+
label: (item) => `${item.dataset.label}${cvDaySummary(item.raw)}: ${fmt(item.raw.y)}`,
120163
},
121164
},
122165
},

js/quadbox/graphs.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ class NbackGraphs extends CvMetricGraphs {
7575
has: g => typeof g.fastestReactionMs === 'number',
7676
axis: { title: { display: true, text: 'fastest reaction on correct presses (ms)' } },
7777
fmt: v => `${Math.round(v)} ms`,
78+
agg: 'min',
7879
empty: noReaction,
7980
},
8081
};
8182
}
8283

8384
includes(g) { return g.status === 'completed'; }
8485
groupKey(g) { return window.cvNbackDisplay.variant(g); }
85-
pointMeta(g) { return { nBack: g.nBack, minutes: Math.round((g.elapsedSeconds ?? 0) / 60) }; }
86-
tooltipSession(r) { return ` ${r.nBack}-back${r.minutes > 0 ? ` (${r.minutes} min)` : ''}`; }
86+
minutes(g) { return (g.elapsedSeconds ?? 0) / 60; }
8787
}
8888
customElements.define('nback-graphs', NbackGraphs);

js/rrt/index.js

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -358,21 +358,16 @@ function startCountDown() {
358358
function stopCountDown() {
359359
timerRunning = false;
360360
timerCount = findStartingTimerCount();
361-
timerBar.style.width = '100%';
362361
clearTimeout(timerInstance);
362+
renderTimerBar();
363363
}
364364

365365
function renderTimerBar() {
366366
const [mode, startingTimerCount] = findStartingTimerState();
367-
if (mode === 'override') {
368-
timerBar.classList.add('override');
369-
customTimeInfo.classList.add('visible');
370-
customTimeInfo.innerHTML = '' + startingTimerCount + 's';
371-
} else {
372-
timerBar.classList.remove('override');
373-
customTimeInfo.classList.remove('visible');
374-
customTimeInfo.innerHTML = '';
375-
}
367+
timerBar.classList.toggle('override', mode === 'override');
368+
// seconds label shows for the global timer too, not just per-type overrides
369+
customTimeInfo.classList.toggle('visible', timerRunning);
370+
customTimeInfo.textContent = timerRunning ? startingTimerCount + 's' : '';
376371
timerBar.style.width = (timerCount / startingTimerCount * 100) + '%';
377372
}
378373

@@ -512,8 +507,6 @@ function init() {
512507
stopCountDown();
513508
if (timerToggled) {
514509
startCountDown();
515-
} else {
516-
renderTimerBar();
517510
}
518511

519512
carouselInit();

0 commit comments

Comments
 (0)