javascript:(function () { 'use strict'; var CFG = { TARGET: 4.5, STRICT: false, RECENT: 5, WEEKS: 16, DATE_HEADERS: '.date-item, [class*="date-item"]' }; var now = new Date(); var TODAY = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 12); var TGT_TXT = String(CFG.TARGET).replace('.', ','); var C = { ACC: '#2563eb', M5: '#16a34a', M4: '#65a30d', M3: '#d97706', M2: '#dc2626', INK: '#1f2937', MUT: '#64748b', GRID: '#e2e8f0', OK: '#15803d', WARN: '#b45309', BAD: '#b91c1c', NA: '#94a3b8' }; var WD = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; var MN = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']; var MON3 = { янв: 1, фев: 2, мар: 3, апр: 4, май: 5, июн: 6, июл: 7, авг: 8, сен: 9, окт: 10, ноя: 11, дек: 12 }; function parseMark(raw) { return String(raw).split('/').map(function (s) { return parseInt(s, 10); }) .filter(function (n) { return n >= 1 && n <= 5; }); } function needFives(sum, n) { if (!n) return 0; var avg = sum / n; if (CFG.STRICT ? avg > CFG.TARGET : avg >= CFG.TARGET) return 0; var k = (CFG.TARGET * n - sum) / (5 - CFG.TARGET); return CFG.STRICT ? Math.floor(k + 1e-9) + 1 : Math.ceil(k - 1e-9); } function fmt(v) { return (v === null || v === undefined || isNaN(v)) ? '—' : Number(v).toFixed(2).replace('.', ','); } function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function avgColor(a) { return (a === null || isNaN(a)) ? C.NA : a >= CFG.TARGET ? C.OK : a >= 3.5 ? C.WARN : C.BAD; } function markColor(v) { return v === 5 ? C.M5 : v === 4 ? C.M4 : v === 3 ? C.M3 : C.M2; } function pad2(n) { return ('0' + n).slice(-2); } function sum(a) { return a.reduce(function (x, y) { return x + y; }, 0); } function goodDate(y, m, d) { if (!(y >= 1990 && y <= 2100 && m >= 1 && m <= 12 && d >= 1 && d <= 31)) return null; var dt = new Date(y, m - 1, d, 12); return (dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d) ? dt : null; } function parseHeader(text, guessYear) { var t = String(text).replace(/\s+/g, ' ').trim().toLowerCase(); if (!t) return null; var rel = /^сегодня/.test(t) ? 0 : /^вчера/.test(t) ? 1 : /^позавчера/.test(t) ? 2 : -1; if (rel >= 0) { var d = new Date(TODAY.getTime()); d.setDate(d.getDate() - rel); return d; } var m = t.match(/(\d{1,2})\s+([а-яё]{3,9})(?:[.,]|\s)+(?:(\d{4})\s*)?/); if (!m) m = t.match(/(\d{1,2})\s+([а-яё]{3,9})/); if (!m) return null; var day = +m[1], mon = MON3[m[2].slice(0, 3)], yr = m[3] ? +m[3] : null; if (!mon) return null; if (yr) return goodDate(yr, mon, day); if (guessYear) return goodDate(guessYear, mon, day); var asy = TODAY.getMonth() >= 7 ? TODAY.getFullYear() : TODAY.getFullYear() - 1; var y = mon >= 8 ? asy : asy + 1; var dt = goodDate(y, mon, day); if (dt && dt.getTime() > TODAY.getTime() + 86400000) dt = goodDate(y - 1, mon, day); return dt; } function dateMeta(dt) { var m = dt.getMonth() + 1, y = dt.getFullYear(); var q = (m === 9 || m === 10) ? 1 : (m === 11 || m === 12) ? 2 : (m <= 3) ? 3 : (m <= 5) ? 4 : 0; var monday = new Date(dt.getTime()); monday.setDate(monday.getDate() - ((dt.getDay() + 6) % 7)); return { ts: dt.getTime(), label: pad2(dt.getDate()) + '.' + pad2(m) + '.' + String(y).slice(2), monthKey: y + '-' + pad2(m), monthLabel: MN[m - 1] + ' ' + String(y).slice(2), quarter: q, wd: (dt.getDay() + 6) % 7, weekTs: monday.getTime(), weekLabel: pad2(monday.getDate()) + '.' + pad2(monday.getMonth() + 1) }; } function collect() { var recs = [], skipped = [], noSubj = 0, itemsCount = 0; var nodes = document.querySelectorAll(CFG.DATE_HEADERS + ', app-mark-item'); var curDate = null, prevTs = null; Array.prototype.forEach.call(nodes, function (node) { if (node.nodeType !== 1) return; if (node.matches(CFG.DATE_HEADERS)) { var gy = prevTs ? new Date(prevTs).getFullYear() : null; var dt = parseHeader(node.textContent, gy); if (dt) { while (prevTs && dt.getTime() > prevTs + 43200000) dt = new Date(dt.getFullYear() - 1, dt.getMonth(), dt.getDate(), 12); prevTs = dt.getTime(); curDate = dt; } return; } var item = node; itemsCount++; var inMk = function (el) { var x = el.closest('.mark'); return !!x && item.contains(x); }; var spans = Array.prototype.slice.call(item.querySelectorAll('span.title-h6')); var markEl = spans.filter(inMk)[0]; var subjEl = spans.filter(function (s) { return !inMk(s) && s.textContent.trim(); })[0]; if (!markEl || !subjEl) { noSubj++; return; } var subject = subjEl.textContent.replace(/\s+/g, ' ').trim(); var raw = (markEl.getAttribute('title') || markEl.textContent).replace(/\s+/g, ' ').trim(); var values = parseMark(raw); if (!values.length) { skipped.push(subject + ' — «' + raw + '»'); return; } var kindEl = item.querySelector('.small-text'); var kind = kindEl ? kindEl.textContent.replace(/\s+/g, ' ').trim() : '—'; var meta = curDate ? dateMeta(curDate) : null; values.forEach(function (v) { recs.push({ subject: subject, v: v, raw: raw, kind: kind, d: meta }); }); }); recs.forEach(function (r, i) { r.idx = i; }); return { recs: recs, skipped: skipped, noSubj: noSubj, itemsCount: itemsCount }; } function byKey(arr, keyFn) { var map = new Map(); arr.forEach(function (r) { var k = keyFn(r); if (k === null) return; if (!map.has(k)) map.set(k, []); map.get(k).push(r); }); return map; } function cntV(list, v) { return list.filter(function (r) { return r.v === v; }).length; } function avgV(list) { return list.length ? sum(list.map(function (r) { return r.v; })) / list.length : null; } function dayKey(ts) { var d = new Date(ts); return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); } function buildModel(recs) { var M = {}; M.subjects = Array.from(byKey(recs, function (r) { return r.subject; }).entries()).map(function (e) { var name = e[0], list = e[1].slice().sort(function (a, b) { return ((a.d ? a.d.ts : 1e15) - (b.d ? b.d.ts : 1e15)) || (a.idx - b.idx); }); var s = sum(list.map(function (r) { return r.v; })); return { name: name, list: list, cnt: list.length, c5: cntV(list, 5), c4: cntV(list, 4), c3: cntV(list, 3), c2: cntV(list, 2), sum: s, avg: s / list.length, need: needFives(s, list.length), ctrl: list.filter(function (r) { return /контрольн/i.test(r.kind); }).length, trend: trendOf(list) }; }).sort(function (a, b) { return (b.avg - a.avg) || (b.cnt - a.cnt) || a.name.localeCompare(b.name, 'ru'); }); M.total = { cnt: recs.length, sum: sum(recs.map(function (r) { return r.v; })) }; M.total.avg = M.total.cnt ? M.total.sum / M.total.cnt : null; M.total.c5 = cntV(recs, 5); M.total.c4 = cntV(recs, 4); M.total.c3 = cntV(recs, 3); M.total.c2 = cntV(recs, 2); M.total.needAll = needFives(M.total.sum, M.total.cnt); var dated = recs.filter(function (r) { return !!r.d; }).sort(function (a, b) { return a.d.ts - b.d.ts || a.idx - b.idx; }); M.hasDates = dated.length > 0; M.dateShare = recs.length ? dated.length / recs.length : 0; M.minTs = dated.length ? dated[0].d.ts : null; M.maxTs = dated.length ? dated[dated.length - 1].d.ts : null; M.months = Array.from(byKey(dated, function (r) { return r.d.monthKey; }).entries()) .sort(function (a, b) { return a[0] < b[0] ? -1 : 1; }) .map(function (e) { var l = e[1]; return { key: e[0], label: l[0].d.monthLabel, cnt: l.length, sum: sum(l.map(function (r) { return r.v; })), avg: avgV(l), c5: cntV(l, 5), c4: cntV(l, 4), c3: cntV(l, 3), c2: cntV(l, 2) }; }); M.weeks = Array.from(byKey(dated, function (r) { return r.d.weekTs; }).entries()) .sort(function (a, b) { return a[0] - b[0]; }) .map(function (e) { var l = e[1]; return { ts: +e[0], label: l[0].d.weekLabel, cnt: l.length, avg: avgV(l) }; }); M.wd = []; for (var i = 0; i < 7; i++) M.wd.push({ list: [] }); dated.forEach(function (r) { M.wd[r.d.wd].list.push(r); }); M.wd = M.wd.map(function (w, i) { return { i: i, name: WD[i], cnt: w.list.length, avg: avgV(w.list) }; }); M.quarters = M.subjects.map(function (s) { var qs = [[], [], [], []]; s.list.forEach(function (r) { if (r.d && r.d.quarter >= 1) qs[r.d.quarter - 1].push(r); }); return { name: s.name, q: qs.map(avgV), avg: s.avg }; }); var allQ = [[], [], [], []]; dated.forEach(function (r) { if (r.d.quarter >= 1) allQ[r.d.quarter - 1].push(r); }); M.quartersTotal = allQ.map(avgV); M.kinds = Array.from(byKey(recs, function (r) { return r.kind; }).entries()) .map(function (e) { return { kind: e[0], cnt: e[1].length, avg: avgV(e[1]) }; }) .sort(function (a, b) { return b.cnt - a.cnt; }); var days = Array.from(new Set(dated.map(function (r) { return dayKey(r.d.ts); }))).sort(); M.daysWithMarks = days.length; var gap = { days: 0 }; for (var gi = 1; gi < days.length; gi++) { var diff = (new Date(days[gi]).getTime() - new Date(days[gi - 1]).getTime()) / 86400000; if (diff - 1 > gap.days) gap = { days: diff - 1, from: days[gi - 1], to: days[gi] }; } M.longestGap = gap; return M; } function trendOf(list) { var n = CFG.RECENT, vals = list.map(function (r) { return r.v; }); if (vals.length < 2) return null; var mid = vals.length >= n * 2 ? vals.length - n : Math.floor(vals.length / 2); var prev = vals.slice(Math.max(0, mid - n), mid), last = vals.slice(mid, mid + n); if (!prev.length || !last.length) return null; var aLast = sum(last) / last.length, aPrev = sum(prev) / prev.length, d = aLast - aPrev; return { sym: d > 0.15 ? '↑' : d < -0.15 ? '↓' : '→', cls: d > 0.15 ? C.OK : d < -0.15 ? C.BAD : C.NA, title: 'последние ' + last.length + ': ' + fmt(aLast) + ' · предыдущие ' + prev.length + ': ' + fmt(aPrev) }; } function svgOpen(w, h) { return '<svg viewBox="0 0 ' + w + ' ' + h + '" style="display:block;max-width:100%;height:auto" role="img">'; } function sparkline(vals) { var w = 118, h = 30, p = 4, s = ''; var yOf = function (v) { return h - p - Math.min(4, Math.max(0, v - 1)) / 4 * (h - 2 * p); }; s += svgOpen(w, h); s += '<title>' + esc(vals.join(', ')) + '</title>'; var gy = yOf(CFG.TARGET); s += '<line x1="' + p + '" y1="' + gy + '" x2="' + (w - p) + '" y2="' + gy + '" stroke="' + C.OK + '" stroke-width="1" stroke-dasharray="3 2" opacity="0.55"/>'; if (vals.length) { var pts = vals.map(function (v, i) { var x = vals.length === 1 ? w / 2 : p + (w - 2 * p) * i / (vals.length - 1); return [x, yOf(v)]; }); var line = pts.map(function (pt) { return pt[0].toFixed(1) + ',' + pt[1].toFixed(1); }).join(' '); if (vals.length > 1) s += '<polyline points="' + line + '" fill="none" stroke="' + C.ACC + '" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>'; pts.forEach(function (pt, i) { if (vals.length <= 30 || i === pts.length - 1) s += '<circle cx="' + pt[0].toFixed(1) + '" cy="' + pt[1].toFixed(1) + '" r="' + (i === pts.length - 1 ? 2.8 : 1.8) + '" fill="' + C.ACC + '"/>'; }); } return s + '</svg>'; } function lineChart(pts, goalLabel) { var W = 920, H = 240, L = 44, R = 16, T = 16, B = 34, s = ''; var vals = pts.map(function (p) { return p.val; }).concat([CFG.TARGET]); var mn = Math.max(1, Math.floor((Math.min.apply(null, vals) - 0.15) * 2) / 2); var mx = Math.min(5, Math.ceil((Math.max.apply(null, vals) + 0.15) * 2) / 2); if (mx - mn < 0.5) mx = mn + 0.5; var X = function (i) { return pts.length === 1 ? (L + W - R) / 2 : L + (W - L - R) * i / (pts.length - 1); }; var Y = function (v) { return T + (H - T - B) * (1 - (v - mn) / (mx - mn)); }; s += svgOpen(W, H); for (var g = mn; g <= mx + 1e-9; g += 0.5) { s += '<line x1="' + L + '" y1="' + Y(g) + '" x2="' + (W - R) + '" y2="' + Y(g) + '" stroke="' + C.GRID + '" stroke-width="1"/>'; s += '<text x="' + (L - 6) + '" y="' + (Y(g) + 3.5) + '" font-size="10" fill="' + C.MUT + '" text-anchor="end">' + fmt(g) + '</text>'; } s += '<line x1="' + L + '" y1="' + Y(CFG.TARGET) + '" x2="' + (W - R) + '" y2="' + Y(CFG.TARGET) + '" stroke="' + C.OK + '" stroke-width="1.4" stroke-dasharray="6 4" opacity="0.75"/>'; s += '<text x="' + (W - R) + '" y="' + (Y(CFG.TARGET) - 5) + '" font-size="10" fill="' + C.OK + '" text-anchor="end">' + esc(goalLabel) + '</text>'; if (pts.length > 1) { s += '<polyline points="' + pts.map(function (p, i) { return X(i).toFixed(1) + ',' + Y(p.val).toFixed(1); }).join(' ') + '" fill="none" stroke="' + C.ACC + '" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"/>'; } pts.forEach(function (p, i) { s += '<circle cx="' + X(i).toFixed(1) + '" cy="' + Y(p.val).toFixed(1) + '" r="4" fill="#fff" stroke="' + C.ACC + '" stroke-width="2"><title>' + esc(p.label) + ': ' + fmt(p.val) + ' (' + p.cnt + ' оц.)</title></circle>'; if (pts.length <= 14) s += '<text x="' + X(i).toFixed(1) + '" y="' + (Y(p.val) - 9).toFixed(1) + '" font-size="10.5" font-weight="700" fill="' + avgColor(p.val) + '" text-anchor="middle">' + fmt(p.val) + '</text>'; var rot = pts.length > 8, tx = X(i), ty = H - B + 15; s += '<text x="' + tx + '" y="' + ty + '" font-size="10" fill="' + C.MUT + '" text-anchor="' + (rot ? 'end' : 'middle') + '"' + (rot ? ' transform="rotate(-35 ' + tx + ' ' + ty + ')"' : '') + '>' + esc(p.label) + '</text>'; }); return s + '</svg>'; } function stackedMonths(months) { var W = 920, H = 210, L = 30, R = 12, T = 22, B = 34, s = ''; var maxT = Math.max.apply(null, months.map(function (m) { return m.cnt; }).concat([1])); var step = (W - L - R) / months.length, bw = Math.min(64, step * 0.62); var Y = function (v) { return H - B - (H - T - B) * v / maxT; }; s += svgOpen(W, H); [0, Math.ceil(maxT / 2), maxT].forEach(function (g) { s += '<line x1="' + L + '" y1="' + Y(g) + '" x2="' + (W - R) + '" y2="' + Y(g) + '" stroke="' + C.GRID + '" stroke-width="1"/>'; s += '<text x="' + (L - 5) + '" y="' + (Y(g) + 3.5) + '" font-size="10" fill="' + C.MUT + '" text-anchor="end">' + g + '</text>'; }); months.forEach(function (m, i) { var x = L + step * i + (step - bw) / 2, y0 = H - B; [[m.c5, C.M5], [m.c4, C.M4], [m.c3, C.M3], [m.c2, C.M2]].forEach(function (part) { if (!part[0]) return; var y1 = Y(part[0]) - (H - B) + y0; s += '<rect x="' + x.toFixed(1) + '" y="' + y1.toFixed(1) + '" width="' + bw.toFixed(1) + '" height="' + (y0 - y1).toFixed(1) + '" fill="' + part[1] + '" rx="1.5"/>'; y0 = y1; }); s += '<text x="' + (x + bw / 2).toFixed(1) + '" y="' + (y0 - 6).toFixed(1) + '" font-size="11" font-weight="700" fill="' + C.INK + '" text-anchor="middle">' + m.cnt + '</text>'; s += '<text x="' + (x + bw / 2).toFixed(1) + '" y="' + (H - B + 15) + '" font-size="10" fill="' + C.MUT + '" text-anchor="middle">' + esc(m.label) + '</text>'; }); return s + '</svg>'; } function donut(parts, centerBig, centerSmall) { var total = sum(parts.map(function (p) { return p.val; })) || 1; var used = parts.filter(function (p) { return p.val > 0; }); var s = svgOpen(150, 150), off = 25; if (used.length === 1) { s += '<circle cx="75" cy="75" r="62" fill="none" stroke="' + used[0].color + '" stroke-width="20"/>'; } else { s += '<circle cx="75" cy="75" r="62" fill="none" stroke="#f1f5f9" stroke-width="20"/>'; used.forEach(function (p) { var len = p.val / total * 389.6; s += '<circle cx="75" cy="75" r="62" fill="none" stroke="' + p.color + '" stroke-width="20" stroke-dasharray="' + (len - 1.5).toFixed(1) + ' ' + (389.6 - len + 1.5).toFixed(1) + '" stroke-dashoffset="' + off.toFixed(1) + '" transform="rotate(-90 75 75)"><title>' + esc(p.label) + ': ' + p.val + '</title></circle>'; off += len; }); } s += '<text x="75" y="76" font-size="26" font-weight="700" fill="' + C.INK + '" text-anchor="middle">' + esc(centerBig) + '</text>'; s += '<text x="75" y="94" font-size="10.5" fill="' + C.MUT + '" text-anchor="middle">' + esc(centerSmall) + '</text>'; return s + '</svg>'; } function weekActivity(weeks) { var W = 920, H = 200, L = 30, R = 40, T = 16, B = 60, s = ''; var maxC = Math.max.apply(null, weeks.map(function (w) { return w.cnt; }).concat([1])); var step = (W - L - R) / weeks.length, bw = Math.min(46, step * 0.66); var Y = function (v) { return H - B - (H - T - B) * v / maxC; }; var YA = function (v) { return H - B - (H - T - B) * (Math.min(5, Math.max(2, v)) - 2) / 3; }; s += svgOpen(W, H); [0, Math.ceil(maxC / 2), maxC].forEach(function (g) { s += '<line x1="' + L + '" y1="' + Y(g) + '" x2="' + (W - R) + '" y2="' + Y(g) + '" stroke="' + C.GRID + '" stroke-width="1"/>'; s += '<text x="' + (L - 5) + '" y="' + (Y(g) + 3.5) + '" font-size="10" fill="' + C.MUT + '" text-anchor="end">' + g + '</text>'; }); var avgs = []; weeks.forEach(function (w, i) { var x = L + step * i + (step - bw) / 2; s += '<rect x="' + x.toFixed(1) + '" y="' + Y(w.cnt).toFixed(1) + '" width="' + bw.toFixed(1) + '" height="' + (H - B - Y(w.cnt)).toFixed(1) + '" fill="' + C.ACC + '" opacity="0.85" rx="2.5"><title>неделя от ' + w.label + ': ' + w.cnt + ' оценок, ср. ' + fmt(w.avg) + '</title></rect>'; s += '<text x="' + (x + bw / 2).toFixed(1) + '" y="' + (Y(w.cnt) - 5).toFixed(1) + '" font-size="10" font-weight="700" fill="' + C.ACC + '" text-anchor="middle">' + w.cnt + '</text>'; var lblEvery = Math.ceil(weeks.length / 10); if (i % lblEvery === 0) { var tx = x + bw / 2, ty = H - B + 14; s += '<text x="' + tx + '" y="' + ty + '" font-size="9.5" fill="' + C.MUT + '" text-anchor="end" transform="rotate(-45 ' + tx + ' ' + ty + ')">' + esc(w.label) + '</text>'; } if (w.avg !== null) avgs.push([x + bw / 2, YA(w.avg), w]); }); if (avgs.length > 1) { s += '<polyline points="' + avgs.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ') + '" fill="none" stroke="' + C.INK + '" stroke-width="1.6" opacity="0.65" stroke-dasharray="1 0"/>'; avgs.forEach(function (p) { s += '<circle cx="' + p[0].toFixed(1) + '" cy="' + p[1].toFixed(1) + '" r="3" fill="#fff" stroke="' + avgColor(p[2].avg) + '" stroke-width="2"><title>неделя от ' + p[2].label + ': ср. ' + fmt(p[2].avg) + '</title></circle>'; }); } [2, 5].forEach(function (g) { s += '<text x="' + (W - R + 5) + '" y="' + (YA(g) + 3.5) + '" font-size="10" fill="' + C.MUT + '">' + g + ',0</text>'; }); return s + '</svg>'; } function td(v, extra) { return '<td style="padding:6px 10px;border:1px solid ' + C.GRID + ';text-align:center;' + (extra || '') + '">' + v + '</td>'; } function th(v, left) { return '<th style="padding:7px 10px;border:1px solid ' + C.GRID + ';text-align:' + (left ? 'left' : 'center') + ';background:#f8fafc;white-space:nowrap">' + v + '</th>'; } function chip(v) { return '<span style="display:inline-block;min-width:20px;padding:1px 7px;border-radius:9px;background:' + markColor(v) + ';color:#fff;font-weight:700">' + v + '</span>'; } function card(label, value, sub, color) { return '<div style="flex:1 1 130px;background:#f8fafc;border:1px solid ' + C.GRID + ';border-radius:12px;padding:10px 12px">' + '<div style="font-size:11px;color:' + C.MUT + '">' + label + '</div>' + '<div style="font-size:22px;font-weight:800;color:' + (color || C.INK) + ';line-height:1.15;margin-top:2px">' + value + '</div>' + (sub ? '<div style="font-size:11px;color:' + C.MUT + ';margin-top:2px">' + sub + '</div>' : '') + '</div>'; } function section(t) { return '<div style="font-weight:700;font-size:13.5px;margin:16px 0 8px;color:' + C.INK + '">' + t + '</div>'; } function legend(items) { return '<div style="display:flex;gap:14px;flex-wrap:wrap;font-size:11.5px;color:' + C.MUT + ';margin-top:6px">' + items.map(function (it) { return '<span style="display:inline-flex;align-items:center;gap:5px"><span style="width:11px;height:11px;border-radius:3px;background:' + it[1] + ';display:inline-block"></span>' + esc(it[0]) + '</span>'; }).join('') + '</div>'; } function wdTiles(wd, compact) { var maxC = Math.max.apply(null, wd.map(function (w) { return w.cnt; }).concat([1])); return '<div style="display:flex;gap:6px">' + wd.map(function (w) { var a = 0.07 + 0.6 * (w.cnt / maxC); return '<div style="flex:1 1 0;background:rgba(37,99,235,' + a.toFixed(2) + ');border-radius:9px;padding:' + (compact ? '6px 2px' : '9px 4px') + ';text-align:center" title="' + w.name + ': ' + w.cnt + ' оценок, средний ' + fmt(w.avg) + '">' + '<div style="font-size:10.5px;color:' + C.MUT + '">' + w.name + '</div>' + '<div style="font-size:' + (compact ? 13 : 17) + 'px;font-weight:800;color:' + C.INK + '">' + w.cnt + '</div>' + (compact ? '' : '<div style="font-size:10.5px;font-weight:700;color:' + avgColor(w.avg) + '">' + fmt(w.avg) + '</div>') + '</div>'; }).join('') + '</div>'; } function tabOverview(M) { var h = '<div style="display:flex;gap:8px;flex-wrap:wrap">'; h += card('Средний балл', fmt(M.total.avg), 'цель ' + TGT_TXT, avgColor(M.total.avg)); h += card('Всего оценок', M.total.cnt, M.daysWithMarks ? M.daysWithMarks + ' дн. с оценками' : ''); h += card('Пятёрок', M.total.c5, 'четвёрок: ' + M.total.c4, C.M5); if (M.total.c3 + M.total.c2) h += card('Троек / двоек', M.total.c3 + ' / ' + M.total.c2, '', M.total.c2 ? C.BAD : C.WARN); h += card('Контрольных', sum(M.subjects.map(function (s) { return s.ctrl; })), 'работ'); if (M.subjects.length) { h += card('Лучший предмет', fmt(M.subjects[0].avg), esc(M.subjects[0].name), avgColor(M.subjects[0].avg)); var worst = M.subjects[M.subjects.length - 1]; if (worst !== M.subjects[0]) h += card('Требует внимания', fmt(worst.avg), esc(worst.name), avgColor(worst.avg)); } h += '</div>'; if (M.total.needAll > 0) h += '<div style="margin-top:10px;font-size:12px;color:' + C.MUT + '">До среднего ' + TGT_TXT + ' по всем предметам не хватает <b style="color:' + C.INK + '">' + M.total.needAll + '</b> пятёрок подряд (без других оценок).</div>'; h += '<div style="display:flex;gap:18px;flex-wrap:wrap;align-items:flex-start;margin-top:14px">'; var dist = donut([{ label: 'Пятёрки', val: M.total.c5, color: C.M5 }, { label: 'Четвёрки', val: M.total.c4, color: C.M4 }, { label: 'Тройки', val: M.total.c3, color: C.M3 }, { label: 'Двойки', val: M.total.c2, color: C.M2 }], String(M.total.cnt), 'оценок'); h += '<div style="flex:0 0 170px">' + dist + legend([['5', C.M5], ['4', C.M4], ['3', C.M3], ['2', C.M2]]) + '</div>'; h += '<div style="flex:1 1 320px"><div style="font-size:12px;color:' + C.MUT + ';margin-bottom:6px">Оценки по дням недели (число и средний балл)</div>' + wdTiles(M.wd, false) + '</div>'; h += '</div>'; if (M.kinds.length) { h += section('По типам работ'); h += '<table style="border-collapse:collapse;font-size:13px"><thead><tr>' + th('Тип работы', 1) + th('Оценок') + th('Средний балл') + '</tr></thead><tbody>'; M.kinds.forEach(function (k) { h += '<tr>' + td(esc(k.kind), 'text-align:left') + td(k.cnt) + td(fmt(k.avg), 'font-weight:700;color:' + avgColor(k.avg)) + '</tr>'; }); h += '</tbody></table>'; } return h; } function tabDynamics(M, noDatesNote) { if (!M.hasDates) return noDatesNote; var h = section('Средний балл по месяцам'); h += lineChart(M.months.map(function (m) { return { label: m.label, val: m.avg, cnt: m.cnt }; }), 'цель ' + TGT_TXT); h += section('Сколько оценок выставляли в месяц (по достоинству)'); h += stackedMonths(M.months); h += legend([['5', C.M5], ['4', C.M4], ['3', C.M3], ['2', C.M2]]); h += section('Средний балл по четвертям'); h += '<table style="border-collapse:collapse;font-size:13px"><thead><tr>' + th('Предмет', 1) + th('1 четв.') + th('2 четв.') + th('3 четв.') + th('4 четв.') + th('Весь период') + '</tr></thead><tbody>'; M.quarters.forEach(function (q) { h += '<tr>' + td(esc(q.name), 'text-align:left'); q.q.forEach(function (a) { h += td(fmt(a), 'font-weight:' + (a === null ? 400 : 700) + ';color:' + avgColor(a)); }); h += td(fmt(q.avg), 'font-weight:700;color:' + avgColor(q.avg)) + '</tr>'; }); h += '<tr style="background:#f8fafc;font-weight:700">' + td('ВСЕ ПРЕДМЕТЫ', 'text-align:left'); M.quartersTotal.forEach(function (a) { h += td(fmt(a), 'color:' + avgColor(a)); }); h += td(fmt(M.total.avg), 'color:' + avgColor(M.total.avg)) + '</tr>'; h += '</tbody></table>'; return h; } function tabSubjects(M) { var h = '<table style="border-collapse:collapse;width:100%;font-size:13px;min-width:860px"><thead><tr>' + th('Предмет', 1) + th('Всего') + th('5') + th('4') + th('3') + th('2') + th('Контр.') + th('Средний') + th('Тренд') + th('До ' + TGT_TXT) + th('Оценки по порядку') + '</tr></thead><tbody>'; M.subjects.forEach(function (s) { h += '<tr>' + td(esc(s.name), 'text-align:left') + td(s.cnt) + td(s.c5) + td(s.c4) + td(s.c3) + td(s.c2) + td(s.ctrl) + td(fmt(s.avg), 'font-weight:700;color:' + avgColor(s.avg)); h += td(s.trend ? '<span style="font-weight:800;color:' + s.trend.cls + '" title="' + esc(s.trend.title) + '">' + s.trend.sym + '</span>' : '—'); h += td(s.need ? String(s.need) : '—', 'font-weight:700') + td(sparkline(s.list.map(function (r) { return r.v; }))) + '</tr>'; }); h += '<tr style="background:#f8fafc;font-weight:700">' + td('ИТОГО', 'text-align:left') + td(M.total.cnt) + td(M.total.c5) + td(M.total.c4) + td(M.total.c3) + td(M.total.c2) + td(sum(M.subjects.map(function(s){return s.ctrl;}))) + td(fmt(M.total.avg), 'color:' + avgColor(M.total.avg)) + td('—') + td(M.total.needAll ? String(M.total.needAll) : '—') + td('') + '</tr>'; h += '</tbody></table>'; h += '<div style="margin-top:8px;font-size:11px;color:' + C.MUT + '">Тренд: средний из последних ' + CFG.RECENT + ' оценок против предыдущих ' + CFG.RECENT + '. Пунктир на мини-графике — цель ' + TGT_TXT + '. «Контр.» — число контрольных работ.</div>'; return h; } function tabActivity(M, noDatesNote) { if (!M.hasDates) return noDatesNote; var h = section('Оценки по неделям (столбики — количество, линия — средний балл)'); var weeks = M.weeks.slice(-CFG.WEEKS); h += weekActivity(weeks); if (M.weeks.length > weeks.length) h += '<div style="font-size:11px;color:' + C.MUT + ';margin-top:4px">Показаны последние ' + weeks.length + ' недель из ' + M.weeks.length + '.</div>'; h += section('По дням недели'); h += wdTiles(M.wd, false); h += section('Заметки'); h += '<ul style="margin:4px 0 0;padding-left:18px;font-size:12.5px;color:' + C.INK + '">'; var bestW = M.weeks.slice().sort(function (a, b) { return b.cnt - a.cnt; })[0]; if (bestW) h += '<li>Максимум оценок за неделю: <b>' + bestW.cnt + '</b> (неделя от ' + bestW.label + ').</li>'; h += '<li>В среднем оценок в неделю: <b>' + fmt(M.weeks.length ? sum(M.weeks.map(function (w) { return w.cnt; })) / M.weeks.length : null) + '</b>.</li>'; var bestD = M.wd.slice().sort(function (a, b) { return b.cnt - a.cnt; })[0]; if (bestD && bestD.cnt) h += '<li>Больше всего оценок ставят в <b>' + bestD.name + '</b> (' + bestD.cnt + '), средний балл в этот день — ' + fmt(bestD.avg) + '.</li>'; if (M.longestGap.days >= 2) h += '<li>Самый длинный перерыв без оценок: <b>' + M.longestGap.days + ' дн.</b> (между ' + M.longestGap.from.split('-').reverse().join('.') + ' и ' + M.longestGap.to.split('-').reverse().join('.') + ').</li>'; h += '</ul>'; return h; } function tabData(M, recs, skipped) { var rows = recs.slice().sort(function (a, b) { return ((a.d ? a.d.ts : 1e15) - (b.d ? b.d.ts : 1e15)) || (a.idx - b.idx); }); var h = '<div style="max-height:52vh;overflow:auto;border:1px solid ' + C.GRID + ';border-radius:10px">' + '<table style="border-collapse:collapse;font-size:12.5px;min-width:560px"><thead><tr>' + th('Дата') + th('День') + th('Предмет', 1) + th('Тип работы', 1) + th('Оценка') + '</tr></thead><tbody>'; rows.forEach(function (r) { h += '<tr>' + td(r.d ? r.d.label : '—') + td(r.d ? WD[r.d.wd] : '—') + td(esc(r.subject), 'text-align:left') + td(esc(r.kind), 'text-align:left;color:' + C.MUT) + td(chip(r.v)) + '</tr>'; }); h += '</tbody></table></div>'; h += '<div style="font-size:11px;color:' + C.MUT + ';margin-top:6px">Всего ' + rows.length + ' оценок (двойные вида «4/5» учтены как две).</div>'; if (skipped.length) h += '<div style="font-size:11px;color:' + C.WARN + ';margin-top:4px">Не учтены в среднем: ' + esc(skipped.join('; ')) + '.</div>'; return h; } function dl(name, lines) { var text = '%EF%BB%BF' + lines.join('\r\n'); var a = document.createElement('a'); if (window.URL && typeof URL.createObjectURL === 'function') { a.href = URL.createObjectURL(new Blob([text], { type: 'text/csv;charset=utf-8' })); } else { a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(text); } a.download = name; document.body.appendChild(a); a.click(); a.remove(); } function csvSummary(M) { var L = ['Предмет;Всего;5;4;3;2;Контрольных;Средний балл;До ' + TGT_TXT]; M.subjects.forEach(function (s) { L.push([s.name, s.cnt, s.c5, s.c4, s.c3, s.c2, s.ctrl, fmt(s.avg), s.need].join(';')); }); L.push(['ИТОГО', M.total.cnt, M.total.c5, M.total.c4, M.total.c3, M.total.c2, sum(M.subjects.map(function(s){return s.ctrl;})), fmt(M.total.avg), ''].join(';')); return L; } function csvMonths(M) { var L = ['Месяц;Оценок;5;4;3;2;Средний балл']; M.months.forEach(function (m) { L.push([m.label, m.cnt, m.c5, m.c4, m.c3, m.c2, fmt(m.avg)].join(';')); }); return L; } function csvAll(recs) { var L = ['Дата;День недели;Предмет;Тип работы;Оценка']; recs.slice().sort(function (a, b) { return ((a.d ? a.d.ts : 1e15) - (b.d ? b.d.ts : 1e15)) || (a.idx - b.idx); }) .forEach(function (r) { L.push([r.d ? r.d.label : '', r.d ? WD[r.d.wd] : '', r.subject, r.kind, r.v].join(';')); }); return L; } function render(data, M, keepTab) { document.getElementById('marks-panel') && document.getElementById('marks-panel').remove(); var panel = document.createElement('div'); panel.id = 'marks-panel'; panel.style.cssText = 'position:fixed;top:14px;right:14px;left:auto;z-index:999999;background:#fff;border-radius:14px;box-shadow:0 14px 44px rgba(0,0,0,.3);padding:14px 16px 12px;font:14px/1.45 Arial,sans-serif;color:' + C.INK + ';max-height:94vh;max-width:95vw;width:1010px;overflow:auto'; var period = M.hasDates ? dateMeta(new Date(M.minTs)).label + ' — ' + dateMeta(new Date(M.maxTs)).label : 'даты не определены'; var TABS = [['view', 'Обзор'], ['dyn', 'Динамика'], ['subj', 'Предметы'], ['act', 'Активность'], ['data', 'Все оценки']]; var cur = keepTab || 'view'; var noDatesNote = '<div style="padding:18px 14px;background:#fffbeb;border:1px solid #fde68a;border-radius:10px;font-size:12.5px;color:#92400e">Даты у оценок не найдены — временной анализ недоступен. Скрипт ищет заголовки по селектору <b>' + esc(CFG.DATE_HEADERS) + '</b>. Если на странице они не совпадают, поправьте CFG.DATE_HEADERS в начале скрипта.</div>'; function headHtml() { var h = '<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">'; h += '<div><div style="font-size:16px;font-weight:800">Дашборд успеваемости</div>'; h += '<div style="font-size:11.5px;color:' + C.MUT + ';margin-top:2px">Карточек: <b>' + data.itemsCount + '</b> · оценок: <b>' + M.total.cnt + '</b> · предметов: <b>' + M.subjects.length + '</b> · период: <b>' + period + '</b>'; if (M.hasDates && (TODAY - new Date(M.maxTs)) > 20 * 86400000) h += ' · <span style="color:' + C.WARN + '">данные старше 20 дней — обновите кнопкой ⟳ после прокрутки</span>'; if (M.hasDates && M.dateShare < 1) h += ' · <span style="color:' + C.WARN + '">у ' + Math.round((1 - M.dateShare) * M.total.cnt) + ' оценок дата не определена</span>'; h += '</div></div>'; h += '<div style="display:flex;gap:4px;flex:0 0 auto">' + '<button id="mp-refresh" title="Пересобрать (пролистайте страницу глубже и нажмите, чтобы подтянуть историю)" style="cursor:pointer;border:1px solid ' + C.GRID + ';background:#fff;border-radius:9px;font-size:17px;line-height:1;padding:4px 9px">⟳</button>' + '<button id="mp-close" title="Закрыть" style="cursor:pointer;border:1px solid ' + C.GRID + ';background:#fff;border-radius:9px;font-size:17px;line-height:1;padding:4px 9px">×</button></div></div>'; h += '<div style="display:flex;gap:6px;margin:10px 0 4px;flex-wrap:wrap">' + TABS.map(function (t) { var on = t[0] === cur; return '<button data-tab="' + t[0] + '" class="mp-tab" style="cursor:pointer;border:1px solid ' + (on ? C.ACC : C.GRID) + ';background:' + (on ? C.ACC : '#fff') + ';color:' + (on ? '#fff' : C.INK) + ';border-radius:999px;padding:5px 14px;font-size:13px;font-weight:' + (on ? 700 : 400) + '">' + t[1] + '</button>'; }).join('') + '</div>'; h += '<div style="display:flex;gap:8px;margin:8px 0 10px;flex-wrap:wrap;align-items:center">'; h += ['mp-csv1|Сводка CSV', 'mp-csv2|По месяцам CSV', 'mp-csv3|Все оценки CSV'].map(function (b) { var p = b.split('|'); return '<button id="' + p[0] + '" style="cursor:pointer;padding:5px 12px;border:1px solid ' + C.GRID + ';border-radius:8px;background:#fff;font-size:12px">' + p[1] + '</button>'; }).join(''); h += '<span style="font-size:11px;color:' + C.MUT + '">«До ' + TGT_TXT + '» — пятёрок подряд, чтобы средний стал ' + (CFG.STRICT ? '>' : '≥') + ' ' + TGT_TXT + '</span></div>'; return h; } function bodyHtml() { if (cur === 'view') return tabOverview(M); if (cur === 'dyn') return tabDynamics(M, noDatesNote); if (cur === 'subj') return tabSubjects(M); if (cur === 'act') return tabActivity(M, noDatesNote); return tabData(M, data.recs, data.skipped); } panel.innerHTML = headHtml() + '<div id="mp-body">' + bodyHtml() + '</div>' + '<div style="margin-top:10px;font-size:10.5px;color:#94a3b8">Всё считается локально, ничего никуда не отправляется · дашборд v2.0</div>'; document.body.appendChild(panel); Array.prototype.forEach.call(panel.querySelectorAll('.mp-tab'), function (b) { b.onclick = function () { cur = b.getAttribute('data-tab'); Array.prototype.forEach.call(panel.querySelectorAll('.mp-tab'), function (x) { var on = x === b; x.style.background = on ? C.ACC : '#fff'; x.style.color = on ? '#fff' : C.INK; x.style.borderColor = on ? C.ACC : C.GRID; x.style.fontWeight = on ? '700' : '400'; }); document.getElementById('mp-body').innerHTML = bodyHtml(); }; }); panel.querySelector('#mp-close').onclick = function () { panel.remove(); }; panel.querySelector('#mp-refresh').onclick = function () { run(cur); }; panel.querySelector('#mp-csv1').onclick = function () { dl('sredniy_ball.csv', csvSummary(M)); }; panel.querySelector('#mp-csv2').onclick = function () { dl('dinamika_po_mesyacam.csv', csvMonths(M)); }; panel.querySelector('#mp-csv3').onclick = function () { dl('vse_otsenki.csv', csvAll(data.recs)); }; } function run(keepTab) { var data = collect(); if (!data.recs.length) { alert('Оценки не найдены. Откройте страницу «Оценки», пролистайте её до конца нужного периода и запустите букмарклет снова.'); return; } var M = buildModel(data.recs); console.log('Карточек: ' + data.itemsCount + '; оценок: ' + M.total.cnt + '; предметов: ' + M.subjects.length + (M.hasDates ? '; период: ' + dateMeta(new Date(M.minTs)).label + ' — ' + dateMeta(new Date(M.maxTs)).label : '; даты не найдены')); if (data.skipped.length) console.log('Не учтены:', data.skipped); if (data.noSubj) console.log('Пропущено карточек без предмета/оценки:', data.noSubj); console.table(M.subjects.map(function (s) { return { 'Предмет': s.name, 'Всего': s.cnt, '5': s.c5, '4': s.c4, '3': s.c3, '2': s.c2, 'Контр.': s.ctrl, 'Средний балл': fmt(s.avg), 'Тренд': s.trend ? s.trend.sym : '', ['До ' + TGT_TXT]: s.need }; })); if (M.months.length) console.table(M.months.map(function (m) { return { 'Месяц': m.label, 'Оценок': m.cnt, '5': m.c5, '4': m.c4, '3': m.c3, '2': m.c2, 'Средний балл': fmt(m.avg) }; })); render(data, M, keepTab); } try { run(); } catch (e) { console.error(e); alert('Дашборд: ошибка — ' + (e && e.message ? e.message : e) + '\nПодробности в консоли (F12).'); } })();