// Game engine: state, chart rendering, quiz mechanics, tab navigation. // Content lives in quizzes.js; raw data in game_data.js. const S = GAME_DATA.scenario; const fmt = n => n.toLocaleString('uk-UA'); const pct = n => (n > 0 ? '+' : '') + n.toFixed(1) + '%'; // Monthly helpers const ALL_MONTHS = GAME_DATA.monthly_totals.map(d => d.month); const MO_LABELS = ALL_MONTHS.map(m => { const [y, mo] = m.split('-'); const names = { '01':'Січ','02':'Лют','03':'Бер','04':'Кві','05':'Тра','06':'Чер','07':'Лип','08':'Сер','09':'Вер','10':'Жов','11':'Лис','12':'Гру' }; return names[mo] + ' ' + y.slice(2); }); const seg = (m, s) => GAME_DATA.monthly_by_segment.find(d => d.month === m && d.segment === s) || {}; const tot = m => GAME_DATA.monthly_totals.find(d => d.month === m) || {}; const segSeries = (s, f) => ALL_MONTHS.map(m => seg(m, s)[f]); const totSeries = f => ALL_MONTHS.map(m => tot(m)[f]); const kFmt = v => v >= 1000 ? (v / 1000).toFixed(0) + 'K' : v; // Chart helpers const COLORS = { primary: '#003964', blue: '#00BBCE', green: '#A7C539', red: '#F15B43', orange: '#ff9800' }; const baseOpts = (legend, scaleOverrides) => ({ plugins: { legend: legend ? { position: 'top', labels: { boxWidth: 14, font: { size: 11 } } } : { display: false }, datalabels: { display: false } }, scales: { y: { beginAtZero: true, ticks: { callback: kFmt } }, ...scaleOverrides }, maintainAspectRatio: true, }); const lineDs = (label, data, color, extra) => ({ label, data, borderColor: color, tension: 0.25, pointRadius: 3, borderWidth: 2, ...extra }); const mkChart = (canvas, type, datasets, labels, opts) => new Chart(document.getElementById(canvas), { type, data: { labels, datasets }, options: opts }); function destroyChart(key) { if (charts[key]) { charts[key].destroy(); delete charts[key]; } } // State const state = { tabsVisited: new Set(), subViewsVisited: new Set(), overviewMode: 'monthly', ridersMode: 'monthly', currentSubTab: 'dayofweek', insights: { yoyPositive: false, longRidesCollapse: false, casualDropMore: false, mixShift: false, durationParadox: false, conclusionSeasonal: false, conclusionParadox: false }, quizzes: {}, wrongCount: 0, checked: { conclusionSeasonal: false, conclusionParadox: false }, deadEndLessons: new Set(), submitted: false, }; const charts = {}; // --- Quiz rendering --- function renderQuiz(quizId, container) { const q = QUIZZES[quizId], solved = state.quizzes[quizId] === 'correct'; const correctIdx = q.options.findIndex(o => o.correct); const el = document.createElement('div'); el.className = 'quiz-section'; el.id = 'quiz-' + quizId; el.innerHTML = `
${q.prompt}
${q.options.map((o, i) => solved ? `` : `` ).join('')}
${solved ? q.options[correctIdx].feedback : ''}
`; container.appendChild(el); } function handleQuizChoice(quizId, optIdx) { const q = QUIZZES[quizId], opt = q.options[optIdx]; const fb = document.getElementById('qfb-' + quizId); const btn = document.getElementById('qopt-' + quizId + '-' + optIdx); if (opt.correct) { state.quizzes[quizId] = 'correct'; btn.classList.add('correct'); btn.querySelector('.q-icon').textContent = '\u2713'; fb.className = 'quiz-feedback visible correct-fb'; fb.textContent = opt.feedback; q.options.forEach((_, i) => { if (i !== optIdx) { const o = document.getElementById('qopt-'+quizId+'-'+i); if (o) o.classList.add('faded'); } }); if (q.insightKey) unlockInsight(q.insightKey); if (q.lessonKey) state.deadEndLessons.add(q.lessonKey); const lessonEl = document.getElementById('lesson-' + quizId); if (lessonEl) lessonEl.classList.remove('lesson-hidden'); } else { state.wrongCount++; btn.classList.add('wrong'); btn.querySelector('.q-icon').textContent = '\u2717'; fb.className = 'quiz-feedback visible wrong-fb'; fb.textContent = opt.feedback; setTimeout(() => { btn.classList.remove('wrong'); btn.classList.add('disabled'); btn.querySelector('.q-icon').textContent = ''; }, 2000); } updateViewCount(); } function quizInto(spotId, quizId) { const s = document.getElementById(spotId); if (!s) return; s.innerHTML = ''; renderQuiz(quizId, s); } // --- Insight cards --- function renderInsightCards() { document.getElementById('observationCards').innerHTML = OBSERVATIONS.map(insightCardHTML).join(''); document.getElementById('conclusionCards').innerHTML = CONCLUSIONS.map(insightCardHTML).join(''); document.getElementById('connectionCard').innerHTML = ''; } function insightCardHTML(o) { const isCon = o.tag === 'conclusion'; const unlocked = state.insights[o.key]; if (!unlocked) { // Fully hidden: just a placeholder return `
\uD83D\uDD12 \u0420\u043E\u0437\u0431\u043B\u043E\u043A\u0443\u0454\u0442\u044C\u0441\u044F, \u043A\u043E\u043B\u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0441\u0442\u0435 \u043D\u0430 \u043F\u0438\u0442\u0430\u043D\u043D\u044F \u0443 \u0432\u043A\u043B\u0430\u0434\u0446\u0456
${isCon ? `
` : ''}
`; } const tagLabel = { observation: '\u0441\u043f\u043e\u0441\u0442\u0435\u0440\u0435\u0436\u0435\u043d\u043d\u044f', conclusion: '\u0432\u0438\u0441\u043d\u043e\u0432\u043e\u043a' }[o.tag] || o.tag; return `
${o.text}
${tagLabel} ${isCon ? `
${state.checked[o.key] ? '\u2713' : ''}
` : ''}
`; } function unlockInsight(key) { if (state.insights[key]) return; state.insights[key] = true; // Re-render all cards to update from placeholder to content renderInsightCards(); const card = document.getElementById('card-' + key); if (card) card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); // Conclusion unlock logic if (state.insights.casualDropMore && state.insights.yoyPositive) unlockInsight('conclusionSeasonal'); if (state.insights.mixShift && state.insights.durationParadox) unlockInsight('conclusionParadox'); if (state.insights.conclusionSeasonal && state.insights.conclusionParadox) { const el = document.getElementById('connectionCard'); if (el && !el.innerHTML) el.innerHTML = `
Одна причина — два симптоми. Бойові підрозділи (захисні насамперед) пішли на ротацію/підготовку. Це пояснює і падіння обсягу операцій, і зростання загальної ефективності одночасно.
`; } updateSubmit(); } function toggleCheck(key) { if (!state.insights[key]) return; state.checked[key] = !state.checked[key]; const el = document.getElementById('check-' + key); el.classList.toggle('checked', state.checked[key]); el.textContent = state.checked[key] ? '\u2713' : ''; updateSubmit(); } function updateSubmit() { document.getElementById('submitBtn').disabled = !(state.checked.conclusionSeasonal || state.checked.conclusionParadox); if (state.insights.conclusionSeasonal || state.insights.conclusionParadox) { const h = document.getElementById('submitHint'); h.textContent = 'Позначте висновки, які хочете надіслати, і натисніть кнопку'; h.style.color = ''; } } // --- View tracking --- function recordView(viewId) { (viewId.startsWith('sub:') ? state.subViewsVisited : state.tabsVisited).add(viewId); updateViewCount(); } function updateViewCount() { const a = Object.values(state.quizzes).filter(v => v === 'correct').length, w = state.wrongCount; const total = Object.keys(QUIZZES).length; document.getElementById('viewsTag').textContent = (a > 0 ? `${a}/${total} \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0456` : '') + (w > 0 ? ` \u00b7 ${w} \u043f\u043e\u043c\u0438\u043b\u043e\u043a` : ''); } // --- Tab switching --- function switchTab(tabId) { document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tabId)); document.querySelectorAll('.tab-pane').forEach(p => p.classList.toggle('active', p.id === 'pane-' + tabId)); recordView(tabId); ({ overview: renderOverview, breakdowns: renderBreakdowns, riders: renderRiders, duration: renderDuration })[tabId](); } // --- Overview tab --- function toggleOverview(mode) { state.overviewMode = mode; document.querySelectorAll('#pane-overview .toggle-btn').forEach(b => b.classList.toggle('active', b.textContent === (mode === 'monthly' ? 'Місячна' : 'Денна'))); renderOverview(); } function renderOverview() { destroyChart('overviewRides'); destroyChart('overviewDuration'); const spot = document.getElementById('quiz-overview-spot'); spot.innerHTML = ''; const monthShort = ['Січ','Лют','Бер','Кві','Тра','Чер','Лип','Сер','Вер','Жов','Лис','Гру']; if (state.overviewMode === 'monthly') { charts.overviewRides = mkChart('chartOverviewRides', 'line', [lineDs('Кількість операцій', totSeries('rides'), COLORS.primary, { backgroundColor: 'rgba(0,57,100,0.08)', fill: true })], MO_LABELS, baseOpts(false)); charts.overviewDuration = mkChart('chartOverviewDuration', 'line', [lineDs('% успіху', totSeries('avg_duration'), COLORS.primary, { backgroundColor: 'rgba(0,57,100,0.08)', fill: true })], MO_LABELS, baseOpts(false, { y: { beginAtZero: false, ticks: { callback: v => v + '%' } } })); } else { // Daily view const daily = GAME_DATA.daily_totals; const labels = daily.map((d) => { const dt = new Date(d.date); return (dt.getDate() === 1) ? monthShort[dt.getMonth()] + " '" + String(dt.getFullYear()).slice(2) : ''; }); charts.overviewRides = mkChart('chartOverviewRides', 'line', [{ label: 'Кількість операцій (день)', data: daily.map(d => d.rides), borderColor: COLORS.primary, backgroundColor: 'rgba(0,57,100,0.06)', fill: true, tension: 0.1, pointRadius: 0, borderWidth: 1.5 }], labels, { ...baseOpts(false), scales: { y: { beginAtZero: true, ticks: { callback: kFmt } }, x: { ticks: { maxRotation: 45, autoSkip: true, maxTicksLimit: 14 } } } }); charts.overviewDuration = mkChart('chartOverviewDuration', 'line', [{ label: '% успіху (день)', data: daily.map(d => d.avg_duration), borderColor: COLORS.primary, backgroundColor: 'rgba(0,57,100,0.06)', fill: true, tension: 0.1, pointRadius: 0, borderWidth: 1.5 }], labels, { ...baseOpts(false), scales: { y: { beginAtZero: false, ticks: { callback: v => v + '%' } }, x: { ticks: { maxRotation: 45, autoSkip: true, maxTicksLimit: 14 } } } }); } renderQuiz('overview', spot); } // --- Breakdowns tab --- function switchSubTab(subId) { state.currentSubTab = subId; document.querySelectorAll('.sub-tab-btn').forEach(b => b.classList.toggle('active', b.dataset.subtab === subId)); recordView('sub:' + subId); renderBreakdowns(); } function renderBreakdowns() { const c = document.getElementById('breakdowns-content'); c.innerHTML = ''; destroyChart('bd1'); destroyChart('bd2'); ({ dayofweek: renderDayOfWeek, hourly: renderHourly, biketypes: renderBikeTypes, duration_buckets: renderDurationBuckets, stations: renderStations })[state.currentSubTab](c); } function renderDayOfWeek(container) { const days = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']; const dayLabels = { Monday:'Пн', Tuesday:'Вт', Wednesday:'Ср', Thursday:'Чт', Friday:'Пт', Saturday:'Сб', Sunday:'Нд' }; const octData = days.map(d => { const r = GAME_DATA.day_of_week.find(x => x.month === '2023-10' && x.weekday === d); return r ? r.rides : 0; }); const novData = days.map(d => { const r = GAME_DATA.day_of_week.find(x => x.month === '2023-11' && x.weekday === d); return r ? r.rides : 0; }); const changes = days.map((_, i) => octData[i] > 0 ? ((novData[i] - octData[i]) / octData[i] * 100) : 0); container.innerHTML = `

Кількість операцій за днем тижня: жовтень vs листопад

% зміни по дням (жовтень → листопад)

Аналітичний урок

Порівнюючи місяці, перевір спершу їхню *календарну структуру*: скільки в кожному кожного дня тижня. Місяці з різною кількістю буднів і вихідних можуть створювати фальшиві патерни в "сирих" сумах.

`; charts.bd1 = new Chart(document.getElementById('chartBd1'), { type: 'bar', data: { labels: days.map(d => dayLabels[d]), datasets: [ { label: 'Жовтень', data: octData, backgroundColor: COLORS.primary }, { label: 'Листопад', data: novData, backgroundColor: COLORS.blue }, ]}, options: { plugins: { legend: { position: 'top', labels: { boxWidth: 14 } }, datalabels: { display: false } }, scales: { y: { beginAtZero: true, ticks: { callback: kFmt } } } } }); charts.bd2 = new Chart(document.getElementById('chartBd2'), { type: 'bar', data: { labels: days.map(d => dayLabels[d]), datasets: [{ data: changes, backgroundColor: changes.map(c => c > -10 ? COLORS.green : c > -30 ? COLORS.orange : COLORS.red) }] }, options: { plugins: { legend: { display: false }, datalabels: { display: true, color: '#333', font: { weight: 'bold', size: 11 }, formatter: v => v.toFixed(1) + '%', anchor: 'end', align: 'top' } }, scales: { y: { ticks: { callback: v => v + '%' } } } }, plugins: [ChartDataLabels] }); quizInto('quiz-dayofweek-spot', 'dayofweek'); } function renderHourly(container) { const hours = Array.from({ length: 24 }, (_, i) => i); const hlabels = hours.map(h => h % 3 === 0 ? h + ':00' : ''); const getTotal = mo => hours.map(h => { const d = GAME_DATA.hourly_totals.find(x => x.month === mo && x.hour === h); return d ? d.rides : 0; }); container.innerHTML = `

Операції за годиною доби: жовтень vs листопад

Аналітичний урок

Коли дві криві за часом *ідентичні за формою* — час доби нічого не пояснює. Це чиста зміна обсягу, не зміна патерну. Шукай розрізи, де форма розподілу змінюється.

`; charts.bd1 = new Chart(document.getElementById('chartBd1'), { type: 'line', data: { labels: hlabels, datasets: [ { label: 'Жовтень', data: getTotal('2023-10'), borderColor: COLORS.primary, fill: true, backgroundColor: COLORS.primary + '1a', tension: 0.3, pointRadius: 0 }, { label: 'Листопад', data: getTotal('2023-11'), borderColor: COLORS.blue, fill: false, tension: 0.3, pointRadius: 0 }, ]}, options: { plugins: { legend: { position: 'top', labels: { boxWidth: 14 } }, datalabels: { display: false } }, scales: { y: { beginAtZero: true, ticks: { callback: kFmt } }, x: { ticks: { autoSkip: true, maxTicksLimit: 8 } } }, aspectRatio: 2.2 } }); quizInto('quiz-hourly-spot', 'hourly'); } function renderBikeTypes(container) { // Fixed order for narrative: prep first (training, logistics), then combat (recon, fire_support, defensive) const types = ['training', 'logistics', 'recon', 'fire_support', 'defensive']; const typeLabels = { training: 'Тренування', logistics: 'Логістика', recon: 'Розвідка', fire_support: 'Вогневе ураження', defensive: 'Оборонні дії', }; const octData = types.map(t => { const r = GAME_DATA.bike_type.find(x => x.month === '2023-10' && x.bike_type === t); return r ? r.rides : 0; }); const novData = types.map(t => { const r = GAME_DATA.bike_type.find(x => x.month === '2023-11' && x.bike_type === t); return r ? r.rides : 0; }); const octEff = types.map(t => { const r = GAME_DATA.bike_type.find(x => x.month === '2023-10' && x.bike_type === t); return r ? r.avg_duration : 0; }); const novEff = types.map(t => { const r = GAME_DATA.bike_type.find(x => x.month === '2023-11' && x.bike_type === t); return r ? r.avg_duration : 0; }); const changes = types.map((_, i) => octData[i] > 0 ? ((novData[i] - octData[i]) / octData[i] * 100) : 0); container.innerHTML = `

Кількість операцій за категорією: жовтень vs листопад

Ефективність (% успіху) за категорією

Аналітичний урок

Коли категорії рухаються в *різні* боки — це сигнал структурної зміни. Подивись на частки до й після, не лише на абсолютні цифри. *Кожна категорія окремо* може бути стабільною, а загальна — рухатися лише через зміну ваг.

`; charts.bd1 = new Chart(document.getElementById('chartBd1'), { type: 'bar', data: { labels: types.map(t => typeLabels[t]), datasets: [ { label: 'Жовтень', data: octData, backgroundColor: COLORS.primary }, { label: 'Листопад', data: novData, backgroundColor: COLORS.blue }, ]}, options: { plugins: { legend: { position: 'top', labels: { boxWidth: 14 } }, datalabels: { display: true, color: '#333', font: { size: 10 }, formatter: (v, ctx) => { const i = ctx.dataIndex; const ci = ctx.datasetIndex; if (ci === 1) return changes[i].toFixed(1) + '%'; return ''; }, anchor: 'end', align: 'top' } }, scales: { y: { beginAtZero: true, ticks: { callback: kFmt } } } }, plugins: [ChartDataLabels] }); charts.bd2 = new Chart(document.getElementById('chartBd2'), { type: 'bar', data: { labels: types.map(t => typeLabels[t]), datasets: [ { label: 'Жовтень', data: octEff, backgroundColor: COLORS.primary }, { label: 'Листопад', data: novEff, backgroundColor: COLORS.blue }, ]}, options: { plugins: { legend: { position: 'top', labels: { boxWidth: 14 } }, datalabels: { display: true, color: '#333', font: { size: 10 }, formatter: v => v.toFixed(1) + '%', anchor: 'end', align: 'top' } }, scales: { y: { beginAtZero: true, max: 100, ticks: { callback: v => v + '%' } } } }, plugins: [ChartDataLabels] }); quizInto('quiz-biketypes-spot', 'biketypes'); } function renderDurationBuckets(container) { const buckets = ['small', 'medium', 'large']; const bucketLabels = { small: 'Мала', medium: 'Середня', large: 'Велика' }; const octData = buckets.map(b => { const r = GAME_DATA.duration_buckets.find(x => x.month === '2023-10' && x.bucket === b); return r ? r.rides : 0; }); const novData = buckets.map(b => { const r = GAME_DATA.duration_buckets.find(x => x.month === '2023-11' && x.bucket === b); return r ? r.rides : 0; }); const changes = buckets.map((_, i) => octData[i] > 0 ? ((novData[i] - octData[i]) / octData[i] * 100) : 0); container.innerHTML = `

% зміни за масштабом операцій (жовтень → листопад)

Операції за масштабом: жовтень vs листопад

Аналітичний урок

Якщо різні масштаби операцій рухаються в один бік приблизно однаково — це не пояснення зміни. Шукай розрізи, де *відсоткові зміни розходяться*.

`; const bucketDisplayLabels = buckets.map(b => bucketLabels[b]); charts.bd1 = new Chart(document.getElementById('chartBd1'), { type: 'bar', data: { labels: bucketDisplayLabels, datasets: [{ data: changes, backgroundColor: changes.map(c => c > -30 ? COLORS.green : c > -45 ? COLORS.orange : COLORS.red) }] }, options: { plugins: { legend: { display: false }, datalabels: { display: true, color: '#333', font: { weight: 'bold', size: 12 }, formatter: v => v.toFixed(1) + '%', anchor: 'end', align: 'top' } }, scales: { y: { ticks: { callback: v => v + '%' } } } }, plugins: [ChartDataLabels] }); charts.bd2 = new Chart(document.getElementById('chartBd2'), { type: 'bar', data: { labels: bucketDisplayLabels, datasets: [ { label: 'Жовтень', data: octData, backgroundColor: COLORS.primary }, { label: 'Листопад', data: novData, backgroundColor: COLORS.blue }, ]}, options: { plugins: { legend: { position: 'top', labels: { boxWidth: 14 } }, datalabels: { display: false } }, scales: { y: { beginAtZero: true, ticks: { callback: kFmt } } } } }); quizInto('quiz-duration_buckets-spot', 'duration_buckets'); } function renderStations(container) { const data = GAME_DATA.station_comparison.slice(0, 10); container.innerHTML = `

Підрозділи — % зміни обсягу (жовтень → листопад)

Аналітичний урок

Коли усі підрозділи рухаються в один бік приблизно однаково — причина системна, не локальна. Не шукай "винного" — шукай фактор, що вплинув на всіх.

`; charts.bd1 = new Chart(document.getElementById('chartBd1'), { type: 'bar', data: { labels: data.map(d => d.station.length > 25 ? d.station.substring(0, 25) + '\u2026' : d.station), datasets: [{ data: data.map(d => d.change_pct), backgroundColor: data.map(d => d.change_pct < -40 ? COLORS.red : d.change_pct < -25 ? COLORS.orange : COLORS.green) }] }, options: { indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { callbacks: { label: ctx => ctx.raw.toFixed(1) + '%' } }, datalabels: { display: true, anchor: 'end', align: 'right', color: '#333', font: { weight: 'bold', size: 11 }, formatter: v => v.toFixed(1) + '%' } }, scales: { x: { ticks: { callback: v => v + '%' } } } }, plugins: [ChartDataLabels] }); quizInto('quiz-stations-spot', 'stations'); } // --- Rider Types tab --- function toggleRiders(mode) { state.ridersMode = mode; document.querySelectorAll('#pane-riders .toggle-btn').forEach(b => b.classList.toggle('active', b.textContent === (mode === 'monthly' ? 'Місячна' : 'Денна'))); renderRiders(); } function renderRiders() { destroyChart('riderRides'); destroyChart('riderMix'); const spot = document.getElementById('quiz-riders-spot'); spot.innerHTML = ''; const monthShortR = ['Січ','Лют','Бер','Кві','Тра','Чер','Лип','Сер','Вер','Жов','Лис','Гру']; if (state.ridersMode === 'monthly') { charts.riderRides = mkChart('chartRiderRides', 'line', [ lineDs('Підготовчі', segSeries('preparation', 'rides'), COLORS.primary, { backgroundColor: 'rgba(0,57,100,0.06)', fill: true }), lineDs('Бойові', segSeries('combat', 'rides'), COLORS.blue, { backgroundColor: 'rgba(0,187,206,0.06)', fill: true }), ], MO_LABELS, baseOpts(true)); // Show share of preparation (the GROWING segment) — highlights the mix shift visually const shares = ALL_MONTHS.map(m => { const p = seg(m, 'preparation').rides || 0, t = tot(m).rides || 1; return Math.round(p / t * 100); }); charts.riderMix = new Chart(document.getElementById('chartRiderMix'), { type: 'line', data: { labels: MO_LABELS, datasets: [{ label: 'Частка підготовчих %', data: shares, borderColor: COLORS.primary, backgroundColor: 'rgba(0,57,100,0.1)', fill: true, tension: 0.25, pointRadius: 5 }] }, options: { plugins: { legend: { display: false }, datalabels: { display: true, color: COLORS.primary, font: { weight: 'bold', size: 11 }, formatter: v => v + '%', anchor: 'end', align: 'top' } }, scales: { y: { min: 0, max: 70, ticks: { callback: v => v + '%' } } } }, plugins: [ChartDataLabels] }); } else { // Daily view by segment const daily = GAME_DATA.daily_by_segment; const dates = [...new Set(daily.map(d => d.date))]; const labels = dates.map(d => { const dt = new Date(d); return dt.getDate() === 1 ? monthShortR[dt.getMonth()] + " '" + String(dt.getFullYear()).slice(2) : ''; }); const prepRides = dates.map(d => { const r = daily.find(x => x.date === d && x.segment === 'preparation'); return r ? r.rides : 0; }); const combatRides = dates.map(d => { const r = daily.find(x => x.date === d && x.segment === 'combat'); return r ? r.rides : 0; }); charts.riderRides = mkChart('chartRiderRides', 'line', [ { label: 'Підготовчі', data: prepRides, borderColor: COLORS.primary, tension: 0.1, pointRadius: 0, borderWidth: 1.5, fill: true, backgroundColor: 'rgba(0,57,100,0.06)' }, { label: 'Бойові', data: combatRides, borderColor: COLORS.blue, tension: 0.1, pointRadius: 0, borderWidth: 1.5, fill: true, backgroundColor: 'rgba(0,187,206,0.06)' }, ], labels, { ...baseOpts(true), scales: { y: { beginAtZero: true, ticks: { callback: kFmt } }, x: { ticks: { maxRotation: 45, autoSkip: true, maxTicksLimit: 14 } } } }); // Daily preparation share const shareData = dates.map(d => { const p = daily.find(x => x.date === d && x.segment === 'preparation'); const c = daily.find(x => x.date === d && x.segment === 'combat'); const pv = p ? p.rides : 0, cv = c ? c.rides : 0; return pv + cv > 0 ? Math.round(pv / (pv + cv) * 100) : 0; }); charts.riderMix = mkChart('chartRiderMix', 'line', [{ label: 'Частка підготовчих %', data: shareData, borderColor: COLORS.primary, tension: 0.1, pointRadius: 0, borderWidth: 1.5, fill: true, backgroundColor: 'rgba(0,57,100,0.1)' }], labels, { ...baseOpts(false), scales: { y: { min: 0, max: 70, ticks: { callback: v => v + '%' } }, x: { ticks: { maxRotation: 45, autoSkip: true, maxTicksLimit: 14 } } } }); } renderQuiz('riders_reveal', spot); } // --- Duration tab --- function renderDuration() { destroyChart('durChange'); destroyChart('durTime'); const oc = seg('2023-10', 'combat').avg_duration, nc = seg('2023-11', 'combat').avg_duration; const om = seg('2023-10', 'preparation').avg_duration, nm = seg('2023-11', 'preparation').avg_duration; const cc = (nc - oc) / oc * 100, cm = (nm - om) / om * 100, ca = S.duration_change_pct; // Vertical grouped bar — Oct and Nov side by side for each group, plus overall const groups = ['Бойові', 'Підготовчі', 'Загалом']; const octVals = [oc, om, S.prior_avg_duration]; const novVals = [nc, nm, S.focus_avg_duration]; const changePcts = [cc, cm, ca]; charts.durChange = new Chart(document.getElementById('chartDurChange'), { type: 'bar', data: { labels: groups, datasets: [ { label: 'Жовтень', data: octVals, backgroundColor: COLORS.primary }, { label: 'Листопад', data: novVals, backgroundColor: COLORS.blue }, ]}, options: { plugins: { legend: { position: 'top', labels: { boxWidth: 14 } }, datalabels: { display: true, color: '#333', font: { size: 11, weight: 'bold' }, formatter: (v, ctx) => { const i = ctx.dataIndex; if (ctx.datasetIndex === 1) return v.toFixed(1) + '% (' + (changePcts[i] >= 0 ? '+' : '') + changePcts[i].toFixed(1) + ' п.п.)'; return v.toFixed(1) + '%'; }, anchor: 'end', align: 'top' }, }, scales: { y: { beginAtZero: true, max: 100, title: { display: true, text: '% успіху' }, ticks: { callback: v => v + '%' } } }, }, plugins: [ChartDataLabels] }); // Efficiency over time with all months — per segment + overall charts.durTime = mkChart('chartDurTime', 'line', [ lineDs('Бойові', segSeries('combat', 'avg_duration'), COLORS.blue), lineDs('Підготовчі', segSeries('preparation', 'avg_duration'), COLORS.primary), lineDs('Загалом', totSeries('avg_duration'), COLORS.red, { borderDash: [5, 5], pointRadius: 2, borderWidth: 2 }), ], MO_LABELS, baseOpts(true, { y: { beginAtZero: false, ticks: { callback: v => v + '%' } } })); // Show mix quiz first (now student can see per-group durations), then the composition effect const spot = document.getElementById('quiz-duration-spot'); spot.innerHTML = ''; renderQuiz('riders_mix', spot); renderQuiz('duration_paradox', spot); } // --- Submit & summary --- function submitAnswer() { if (state.submitted) return; state.submitted = true; if (!(state.checked.conclusionSeasonal && state.checked.conclusionParadox)) { state.submitted = false; const hint = document.getElementById('submitHint'); hint.style.color = '#e65100'; hint.textContent = state.checked.conclusionSeasonal ? 'Структуру падіння обсягу зрозуміли. А зростання ефективності — як його пояснити?' : state.checked.conclusionParadox ? 'Зміну композиції знайшли. А падіння обсягу — це справді проблема? Поверніться до Загального огляду.' : 'Спочатку оберіть висновки, які хочете надіслати.'; return; } showSummary(); } function showSummary() { const n22 = GAME_DATA.yoy_totals.find(d => d.month === '2022-11'), n23 = GAME_DATA.yoy_totals.find(d => d.month === '2023-11'); const yoyPct = ((n23.rides - n22.rides) / n22.rides * 100).toFixed(1); const oc = seg('2023-10', 'combat').avg_duration, om = seg('2023-10', 'preparation').avg_duration; const ocShare = Math.round(seg('2023-10', 'combat').rides / S.prior_rides * 100), ncShare = Math.round(seg('2023-11', 'combat').rides / S.focus_rides * 100); const answered = Object.values(state.quizzes).filter(v => v === 'correct').length; const total = Object.keys(QUIZZES).length; const bonus = [...state.deadEndLessons].map(k => LESSON_NAMES[k]).filter(Boolean); document.getElementById('summaryBox').innerHTML = `

\u0420\u043e\u0437\u0441\u043b\u0456\u0434\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e

${answered}/${total} \u043f\u0438\u0442\u0430\u043d\u044c\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u00b7 ${state.wrongCount} \u043f\u043e\u043c\u0438\u043b\u043a\u043e\u0432\u0438\u0445 \u0441\u043f\u0440\u043e\u0431

\u0420\u0435\u0430\u043b\u044c\u043d\u043e\u0457 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u043e\u0457 "\u043f\u0435\u0440\u0435\u043c\u043e\u0433\u0438" \u043d\u0435\u043c\u0430\u0454

\u0411\u0435\u0437 \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457 \u043a\u043e\u043c\u0430\u043d\u0434\u0438\u0440 \u043c\u0456\u0433 \u0437\u0440\u043e\u0431\u0438\u0442\u0438 \u0432\u0438\u0441\u043d\u043e\u0432\u043e\u043a \u043f\u0440\u043e \u0440\u0435\u0430\u043b\u044c\u043d\u0435 \u043f\u043e\u043a\u0440\u0430\u0449\u0435\u043d\u043d\u044f \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0456 \u043e\u043c\u0430\u043d\u043b\u0438\u0432\u043e\u0457 \u0430\u0433\u0440\u0435\u0433\u043e\u0432\u0430\u043d\u043e\u0457 \u0446\u0438\u0444\u0440\u0438. \u041d\u0430\u0441\u043f\u0440\u0430\u0432\u0434\u0456 \u2014 \u043a\u043e\u0436\u0435\u043d \u0441\u0435\u0433\u043c\u0435\u043d\u0442 \u043e\u043a\u0440\u0435\u043c\u043e \u0441\u0442\u0430\u0431\u0456\u043b\u044c\u043d\u0438\u0439; \u0440\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f \u043b\u0438\u0448\u0435 \u043a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0456\u044f.

\u0412\u0438\u0441\u043d\u043e\u0432\u043e\u043a 1: \u043f\u0430\u0434\u0456\u043d\u043d\u044f \u043e\u0431\u0441\u044f\u0433\u0443 \u2014 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043d\u0438\u0439 \u0437\u0441\u0443\u0432

\u0411\u043e\u0439\u043e\u0432\u0456 \u043f\u0456\u0434\u0440\u043e\u0437\u0434\u0456\u043b\u0438 (\u043e\u0441\u043e\u0431\u043b\u0438\u0432\u043e \u043e\u0431\u043e\u0440\u043e\u043d\u043d\u0456) \u0441\u043a\u043e\u0440\u043e\u0442\u0438\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u0456\u0441\u0442\u044c \u2014 \u0447\u0430\u0441\u0442\u043a\u0430 \u0432\u043f\u0430\u043b\u0430 \u0437 ~85% \u0434\u043e ~50%. \u041f\u0456\u0434\u0433\u043e\u0442\u043e\u0432\u0447\u0456 (\u0442\u0440\u0435\u043d\u0443\u0432\u0430\u043d\u043d\u044f, \u043b\u043e\u0433\u0456\u0441\u0442\u0438\u043a\u0430) \u043d\u0430\u0442\u043e\u043c\u0456\u0441\u0442\u044c *\u0432\u0438\u0440\u043e\u0441\u043b\u0438* \u0432 \u043e\u0431\u0441\u044f\u0437\u0456. \u0426\u0435 \u043d\u0435 \u043f\u0440\u043e\u0441\u0442\u043e "\u0441\u0442\u0430\u043b\u043e \u043c\u0435\u043d\u0448\u0435" \u2014 \u0446\u0435 \u043f\u0435\u0440\u0435\u0440\u043e\u0437\u043f\u043e\u0434\u0456\u043b.

\u041b\u0438\u0441 2022
${fmt(n22.rides)}
\u041b\u0438\u0441 2023
${fmt(n23.rides)}
${pct(parseFloat(yoyPct))} YoY

\u0423\u0440\u043e\u043a: \u0437\u0430\u0432\u0436\u0434\u0438 \u043f\u0438\u0442\u0430\u0439 "\u043f\u043e\u0440\u0456\u0432\u043d\u044f\u043d\u043e \u0437 \u0447\u0438\u043c?". \u0406 \u043a\u043e\u043b\u0438 \u0446\u0438\u0444\u0440\u0430 \u0440\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f \u2014 \u0441\u043f\u0435\u0440\u0448\u0443 \u043f\u0435\u0440\u0435\u0432\u0456\u0440, \u0447\u0438 \u0440\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f \u043a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0456\u044f.

\u0412\u0438\u0441\u043d\u043e\u0432\u043e\u043a 2: \u0437\u0440\u043e\u0441\u0442\u0430\u043d\u043d\u044f \u0435\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0456 \u2014 \u0435\u0444\u0435\u043a\u0442 \u043a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0456\u0457

\u041f\u0456\u0434\u0433\u043e\u0442\u043e\u0432\u0447\u0456 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0457 \u043c\u0430\u044e\u0442\u044c \u0432\u0438\u0449\u0443 \u0431\u0430\u0437\u043e\u0432\u0443 \u0435\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u0456\u0441\u0442\u044c (~${om.toFixed(1)}%), \u0431\u043e\u0439\u043e\u0432\u0456 \u2014 \u043d\u0438\u0436\u0447\u0443 (~${oc.toFixed(1)}%). \u0423 \u0436\u043e\u0432\u0442\u043d\u0456 \u043f\u0456\u0434\u0433\u043e\u0442\u043e\u0432\u0447\u0438\u0445 \u0431\u0443\u043b\u043e \u043b\u0438\u0448\u0435 ${ocShare}% \u0432\u0456\u0434 \u0437\u0430\u0433\u0430\u043b\u0443, \u0443 \u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434\u0456 \u2014 \u0432\u0436\u0435 ${ncShare}%. \u0417\u0430\u0433\u0430\u043b\u044c\u043d\u0430 \u0446\u0438\u0444\u0440\u0430 \u0437\u0440\u043e\u0441\u043b\u0430 \u043b\u0438\u0448\u0435 \u0442\u043e\u043c\u0443, \u0449\u043e \u0447\u0430\u0441\u0442\u043a\u0430 "\u043b\u0435\u0433\u0448\u0438\u0445" \u043c\u0456\u0441\u0456\u0439 \u0437\u0431\u0456\u043b\u044c\u0448\u0438\u043b\u0430\u0441\u044c \u2014 \u0430 \u043d\u0435 \u0442\u043e\u043c\u0443, \u0449\u043e \u0445\u0442\u043e\u0441\u044c \u0441\u0442\u0430\u0432 \u0435\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u0456\u0448\u0438\u043c.

\u0423\u0440\u043e\u043a: \u043a\u043e\u043b\u0438 \u0437\u043c\u0456\u043d\u044e\u0454\u0442\u044c\u0441\u044f \u043c\u0456\u043a\u0441 \u0433\u0440\u0443\u043f, \u0430\u0433\u0440\u0435\u0433\u0430\u0442 \u0440\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430\u0432\u0456\u0442\u044c \u044f\u043a\u0449\u043e \u0436\u043e\u0434\u043d\u0430 \u0433\u0440\u0443\u043f\u0430 \u043d\u0435 \u0437\u043c\u0456\u043d\u0438\u043b\u0430 \u043f\u043e\u0432\u0435\u0434\u0456\u043d\u043a\u0438. \u0417\u0430\u0432\u0436\u0434\u0438 \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u0443\u0439, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u0440\u043e\u0431\u0438\u0442\u0438 \u0432\u0438\u0441\u043d\u043e\u0432\u043e\u043a.

\u041e\u0434\u043d\u0430 \u043f\u0440\u0438\u0447\u0438\u043d\u0430 \u2014 \u0434\u0432\u0430 \u0441\u0438\u043c\u043f\u0442\u043e\u043c\u0438

\u0411\u043e\u0439\u043e\u0432\u0456 \u043f\u0456\u0434\u0440\u043e\u0437\u0434\u0456\u043b\u0438 \u043d\u0430 \u0440\u043e\u0442\u0430\u0446\u0456\u0457/\u043f\u0456\u0434\u0433\u043e\u0442\u043e\u0432\u0446\u0456. \u0426\u0435 \u043f\u043e\u044f\u0441\u043d\u044e\u0454 \u0456 \u043f\u0430\u0434\u0456\u043d\u043d\u044f \u043e\u0431\u0441\u044f\u0433\u0443 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439, \u0456 \u0437\u0440\u043e\u0441\u0442\u0430\u043d\u043d\u044f \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0457 \u0446\u0438\u0444\u0440\u0438 \u0435\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0456 \u2014 \u043e\u0431\u0438\u0434\u0432\u0430 \u0435\u0444\u0435\u043a\u0442\u0438 \u0432\u0456\u0434 \u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043d\u043e\u0433\u043e \u0437\u0441\u0443\u0432\u0443.

${bonus.length ? `

\u0423\u0440\u043e\u043a\u0438 \u043d\u0430 \u0442\u0443\u043f\u0438\u043a\u043e\u0432\u0438\u0445 \u0433\u0456\u043b\u043a\u0430\u0445

` : ''}

\u041e\u0431\u0433\u043e\u0432\u043e\u0440\u0435\u043d\u043d\u044f

1. \u041d\u0430 \u044f\u043a\u0443 \u0432\u043a\u043b\u0430\u0434\u043a\u0443 \u0432\u0438 \u043f\u0456\u0448\u043b\u0438 \u043f\u0435\u0440\u0448\u043e\u044e? \u0429\u043e \u0432\u043e\u043d\u0430 \u0432\u0430\u043c \u043f\u043e\u043a\u0430\u0437\u0430\u043b\u0430?

2. \u042f\u043a\u0431\u0438 \u0432 \u0434\u0430\u0448\u0431\u043e\u0440\u0434\u0456 \u0431\u0443\u043b\u0430 \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0434\u043d\u0430 \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u0430 \u0446\u0438\u0444\u0440\u0430 \u2014 \u0432\u0438 \u0431 \u043f\u043e\u043c\u0456\u0442\u0438\u043b\u0438 \u0437\u0441\u0443\u0432 \u043a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0456\u0457?

3. \u0423 \u0432\u0430\u0448\u0456\u0439 \u0440\u0435\u0430\u043b\u044c\u043d\u0456\u0439 \u0440\u043e\u0431\u043e\u0442\u0456 \u2014 \u044f\u043a\u0456 \u043f\u043e\u043a\u0430\u0437\u043d\u0438\u043a\u0438 \u043d\u0430\u0439\u0431\u0456\u043b\u044c\u0448 \u0432\u0440\u0430\u0437\u043b\u0438\u0432\u0456 \u0434\u043e \u0442\u0430\u043a\u0438\u0445 \u0435\u0444\u0435\u043a\u0442\u0456\u0432?

4. \u0429\u043e \u0431 \u0432\u0438 \u0441\u043a\u0430\u0437\u0430\u043b\u0438 \u043a\u043e\u043c\u0430\u043d\u0434\u0438\u0440\u043e\u0432\u0456, \u044f\u043a\u0431\u0438 \u0432\u0456\u043d \u043f\u0440\u043e\u044f\u0432\u0438\u0432 \u0437\u0430\u0446\u0456\u043a\u0430\u0432\u043b\u0435\u043d\u043d\u044f \u043b\u0438\u0448\u0435 \u0446\u0438\u0444\u0440\u043e\u044e 71%?

`; document.getElementById('summaryOverlay').classList.add('visible'); } // --- Restart --- function restart() { state.tabsVisited.clear(); state.subViewsVisited.clear(); state.overviewMode = 'monthly'; state.ridersMode = 'monthly'; state.currentSubTab = 'dayofweek'; state.deadEndLessons.clear(); state.submitted = false; state.wrongCount = 0; state.quizzes = {}; Object.keys(state.insights).forEach(k => state.insights[k] = false); Object.keys(state.checked).forEach(k => state.checked[k] = false); Object.values(charts).forEach(c => { try { c.destroy(); } catch (e) {} }); for (const k in charts) delete charts[k]; document.getElementById('summaryOverlay').classList.remove('visible'); const intro = document.getElementById('introOverlay'); if (intro) intro.classList.remove('hidden'); document.getElementById('submitHint').textContent = 'Пройдіть вкладки і відповідайте на питання, щоб зібрати докази'; document.getElementById('submitHint').style.color = ''; renderInsightCards(); switchTab('overview'); toggleOverview('monthly'); } // --- Init --- Chart.defaults.font.family = "'Source Sans Pro', sans-serif"; Chart.register(ChartDataLabels); Chart.defaults.plugins.datalabels = { display: false }; document.getElementById('amRides').textContent = fmt(S.focus_rides); document.getElementById('amRidesDelta').textContent = pct(S.rides_change_pct) + ' до жовтня'; document.getElementById('amDur').textContent = S.focus_avg_duration.toFixed(1) + '%'; document.getElementById('amDurDelta').textContent = (S.duration_change_pct >= 0 ? '+' : '') + S.duration_change_pct.toFixed(1) + ' п.п. до жовтня'; renderInsightCards(); renderOverview(); state.tabsVisited.add('overview'); updateViewCount();