dashbord-roznichnyh-prodazh-7/script.js

535 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
var STORAGE_KEY = "dash-retail-7-v1";
var MONTHS_FULL = [
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"
];
var MONTHS_SHORT = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
var GROUPS = [
{ id: "connect", name: "Подключение в интернет", unit: "подк." },
{ id: "tv", name: "Телевидение", unit: "подк." },
{ id: "mobile", name: "Мобильная связь", unit: "актив." },
{ id: "smart", name: "Умный дом", unit: "шт." },
{ id: "devices", name: "Товары и устройства", unit: "шт." }
];
var FACT = {
connect: [1240, 1410, 1585, 1730, 1688, 1802, 1755, 1904, 1861, 2015, 2124, 2260],
tv: [980, 1045, 1120, 1188, 1253, 1204, 1316, 1382, 1410, 1466, 1523, 1590],
mobile: [2410, 2585, 2742, 2690, 2914, 3025, 2876, 3102, 3248, 3390, 3512, 3640],
smart: [85, 98, 121, 134, 149, 162, 158, 171, 189, 204, 218, 236],
devices: [1680, 1742, 1815, 1938, 2011, 1966, 2104, 2246, 2318, 2455, 2590, 2742]
};
var PLAN = {
connect: [1200, 1360, 1500, 1640, 1740, 1820, 1860, 1950, 2020, 2100, 2180, 2280],
tv: [950, 1030, 1110, 1180, 1260, 1300, 1360, 1420, 1480, 1540, 1600, 1660],
mobile: [2350, 2520, 2690, 2830, 2960, 3080, 3180, 3290, 3400, 3510, 3620, 3730],
smart: [80, 95, 110, 125, 140, 155, 168, 180, 192, 206, 220, 235],
devices: [1650, 1740, 1860, 1980, 2080, 2150, 2230, 2340, 2450, 2560, 2680, 2800]
};
var PALETTE = [
"var(--kt-ai-chart-blue)",
"var(--kt-ai-chart-green)",
"var(--kt-ai-chart-orange)",
"var(--kt-ai-chart-salmon)",
"var(--kt-ai-chart-pink)"
];
var state = {
period: "q3",
group: "all",
q: ""
};
function loadState() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
var p = JSON.parse(raw);
if (typeof p === "object" && p !== null) {
if (typeof p.period === "string") state.period = p.period;
if (typeof p.group === "string") state.group = p.group;
if (typeof p.q === "string") state.q = p.q;
}
} catch (e) { /* localStorage недоступен — работаем в памяти */ }
}
function saveState() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch (e) { /* ignore */ }
}
/* ---------- Вспомогательные ---------- */
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
function groupIndex() {
for (var i = 0; i < GROUPS.length; i++) {
if (GROUPS[i].id === state.group) return i;
}
return 0;
}
function activeGroup() {
return GROUPS[groupIndex()];
}
function periodIndex() {
if (state.period === "q1") return 3;
if (state.period === "q2") return 6;
if (state.period === "full") return 12;
return 9;
}
function periodMonths() {
return windowMonths(periodIndex());
}
function windowMonths(i) {
if (i === 1) return [0, 1, 2];
if (i === 2) return [3, 4, 5];
if (i === 3) return [6, 7, 8];
if (i === 4) return [0, 1, 2, 3, 4, 5, 6, 7, 8];
return [i - 3, i - 2, i - 1];
}
function periodLabel() {
if (state.period === "q1") return "I квартал";
if (state.period === "q2") return "II квартал";
if (state.period === "q3") return "III квартал";
if (state.period === "full") return "Янв — Сен";
var i = periodIndex();
return MONTHS_FULL[i - 2] + " — " + MONTHS_FULL[i - 1];
}
function monthsOf(g) {
var m = periodMonths();
var out = [];
for (var k = 0; k < m.length; k++) {
out.push({ mi: m[k], fact: FACT[g][m[k]], plan: PLAN[g][m[k]] });
}
return out;
}
function groupsOf() {
if (state.group !== "all") return [state.group];
return GROUPS.map(function (g) { return g.id; });
}
function groupShare() {
var gs = groupsOf();
var ms = periodMonths();
var total = 0;
for (var a = 0; a < gs.length; a++) {
for (var b = 0; b < ms.length; b++) total += FACT[gs[a]][ms[b]];
}
var out = [];
for (var i = 0; i < gs.length; i++) {
var s = 0;
var ms2 = periodMonths();
for (var b = 0; b < ms2.length; b++) s += FACT[gs[i]][ms2[b]];
out.push({
id: gs[i],
sum: s,
pct: total ? Math.round((s / total) * 1000) / 10 : 0
});
}
return out;
}
function prevMonths() {
var i = periodIndex();
if (i === 1 || i === 2 || i === 3) return [i - 3, i - 2, i - 1];
return [];
}
function fmt(n) {
return String(Math.round(n || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
function toneCls(p) {
if (p == null) return "flat";
if (p > 1.5) return "up";
if (p < -1.5) return "down";
return "flat";
}
function toneArrow(p) {
if (p == null) return "—";
if (p > 1.5) return "▲";
if (p < -1.5) return "▼";
return "•";
}
/* ---------- Отрисовка ---------- */
function tipForMonth(mi, g, f, p) {
return {
title: MONTHS_FULL[mi] + " 2026",
rows: [
[g.name, "факт " + fmt(f)],
["План", fmt(p) + (g.unit ? " " + g.unit : "")],
["Выполнение плана", Math.round((f / p) * 100) + "%"]
]
};
}
function tipAttr(item) {
var json;
try {
json = JSON.stringify(item);
} catch (e) {
json = "";
}
var v = item && item.hint ? item.hint : ((item && item.label ? item.label + " · " : "") + (item ? item.value : ""));
if (!json) return ' data-kt-tip="' + esc(v) + '" tabindex="0"';
return ' data-kt-tip="' + esc(json) + '" tabindex="0"';
}
function renderTrendChart() {
var ms = periodMonths();
if (!ms.length) return "";
var gs = groupsOf();
var W = 560, H = 196;
var x0 = 10, y0 = 16, w = W - 24, hh = H - 44;
var factVals = [], planVals = [];
for (var k = 0; k < ms.length; k++) {
var f = 0, p = 0;
for (var g = 0; g < gs.length; g++) {
f += FACT[gs[g]][ms[k]];
p += PLAN[gs[g]][ms[k]];
}
factVals.push(f);
planVals.push(p);
}
var all = factVals.concat(planVals);
var max = 0;
for (var m = 0; m < all.length; m++) if (all[m] > max) max = all[m];
max = max || 1;
var n = ms.length;
function px(i) { return x0 + (n > 1 ? i * (w / (n - 1)) : w / 2); }
function py(v) { return y0 + hh - (v / max) * hh; }
function path(vals) {
var s = "";
for (var i = 0; i < vals.length; i++) {
s += (i ? "L" : "M") + px(i).toFixed(1) + " " + py(vals[i]).toFixed(1);
}
return s;
}
var area = path(factVals) + " L" + px(n - 1).toFixed(1) + " " + (y0 + hh).toFixed(1) + " L" + px(0).toFixed(1) + " " + (y0 + hh).toFixed(1) + " Z";
var grid = "";
for (var fr = 0; fr <= 2; fr++) {
grid += '<line x1="' + x0 + '" y1="' + (y0 + hh * fr * 0.5).toFixed(1) + '" x2="' + (x0 + w) + '" y2="' + (y0 + hh * fr * 0.5).toFixed(1) + '" stroke="var(--kt-ai-border)" stroke-width="1" stroke-dasharray="2 3"/>';
}
function factTip(mi) {
var g = activeGroup();
return '<circle cx="' + px(mi).toFixed(1) + '" cy="' + py(factVals[mi]).toFixed(1) + '" r="3.4" fill="var(--kt-ai-chart-blue)" tabindex="0" data-kt-tip="' + esc(JSON.stringify(tipForMonth(mi, g, factVals[mi], planVals[mi]))) + '"/>';
}
var dots = "";
for (var i = 0; i < n; i++) {
dots += '<circle cx="' + px(i).toFixed(1) + '" cy="' + py(planVals[i]).toFixed(1) + '" r="3" fill="var(--kt-ai-fg-muted)" opacity="0.55"/>';
dots += factTip(i);
}
var labels = "";
for (var j = 0; j < n; j++) {
labels += '<text x="' + px(j).toFixed(1) + '" y="' + (H - 8) + '" text-anchor="middle" font-size="11" fill="var(--kt-ai-fg-faint)">' + esc(MONTHS_SHORT[ms[j]]) + "</text>";
}
var title = n > 1 ? MONTHS_SHORT[ms[0]].charAt(0).toUpperCase() + MONTHS_SHORT[ms[0]].slice(1) + " — " + MONTHS_SHORT[ms[n - 1]].charAt(0).toUpperCase() + MONTHS_SHORT[ms[n - 1]].slice(1) + " 2026" : MONTHS_FULL[ms[0]] + " 2026";
var sumF = 0;
for (var s2 = 0; s2 < factVals.length; s2++) sumF += factVals[s2];
return '<section class="kt-ai-chart-card"><div class="cc-head"><div><div class="cc-title">Динамика продаж по месяцам · ' + esc(title) + "</div>" +
'<div class="cc-total"><span class="num">' + fmt(sumF) + '</span><span class="unit">' + esc(activeGroup().unit) + " за период</span></div></div>" +
'<div class="chart-legend">' +
'<span><i style="background:var(--kt-ai-chart-blue)"></i>Факт</span>' +
'<span><i style="background:var(--kt-ai-fg-muted)"></i>План</span></div></div>' +
'<svg viewBox="0 0 ' + W + " " + H + '" style="width:100%;height:auto;display:block" role="img" aria-label="График динамики продаж">' +
grid + '<path d="' + area + '" fill="var(--kt-ai-chart-blue)" opacity="0.08"/>' +
'<path d="' + path(planVals) + '" fill="none" stroke="var(--kt-ai-fg-muted)" stroke-width="1.5" stroke-dasharray="4 4" opacity="0.6"/>' +
'<path d="' + path(factVals) + '" fill="none" stroke="var(--kt-ai-chart-blue)" stroke-width="2" stroke-linejoin="round"/>' +
dots + labels +
'<text x="' + x0 + '" y="10" font-size="10" fill="var(--kt-ai-fg-faint)">' + fmt(max) + " " + esc(activeGroup().unit) + "</text></svg></section>";
}
function renderDonut() {
var shares = groupShare();
var total = 0;
for (var t = 0; t < shares.length; t++) total += shares[t].sum;
if (total <= 0) return "";
var segs = "";
var off = 25;
for (var i = 0; i < shares.length; i++) {
var p = total ? (shares[i].sum / total) * 100 : 0;
segs += '<circle r="15.9155" cx="21" cy="21" fill="none" stroke="' + PALETTE[i % PALETTE.length] + '" stroke-width="5.5" stroke-dasharray="' + p.toFixed(2) + " " + (100 - p).toFixed(2) + '" stroke-dashoffset="' + off.toFixed(2) + '" data-kt-tip="' + esc(JSON.stringify({ title: groupLabel(shares[i].id), rows: [[groupLabel(shares[i].id), fmt(shares[i].sum])], [shares[i].pct + "% от периода", ""]]) + '" tabindex="0"></circle>';
off -= p;
}
var legend = shares.map(function (s, i) {
return '<div class="donut-legend-row"><span class="dot" style="background:' + PALETTE[i % PALETTE.length] + '"></span><span class="nm">' + esc(groupLabel(s.id)) + '</span><span class="val kt-ai-num">' + fmt(s.sum) + '</span><span class="pct">' + s.pct + "%</span></div>";
}).join("");
return '<section class="kt-ai-chart-card"><div class="cc-head"><div><div class="cc-title">Структура продаж по группам</div>' +
'<div class="cc-total"><span class="num">' + fmt(total) + '</span><span class="unit">за ' + esc(periodLabel().toLowerCase()) + "</span></div></div></div>" +
'<div class="donut-flex">' +
'<svg viewBox="0 0 42 42" style="width:116px;height:116px;flex-shrink:0" role="img" aria-label="Структура продаж">' + segs +
'<text x="21" y="21.5" text-anchor="middle" dominant-baseline="middle" font-size="7" font-weight="600" fill="var(--kt-ai-fg)">' + fmt(total) + "</text></svg>" +
'<div class="donut-legend">' + legend + "</div></div></section>";
}
function groupLabel(id) {
for (var i = 0; i < GROUPS.length; i++) {
if (GROUPS[i].id === id) return GROUPS[i].name;
}
return id;
}
function buildKpis() {
var ms = periodMonths();
var gs = groupsOf();
var curF = 0, curP = 0;
for (var a = 0; a < gs.length; a++) {
for (var b = 0; b < ms.length; b++) {
curF += FACT[gs[a]][ms[b]];
curP += PLAN[gs[a]][ms[b]];
}
}
var pm = prevMonths();
var prevF = 0, prevP = 0;
if (pm.length) {
for (var c = 0; c < gs.length; c++) {
for (var d = 0; d < pm.length; d++) {
prevF += FACT[gs[c]][pm[d]];
prevP += PLAN[gs[c]][pm[d]];
}
}
}
var delta = null, deltaTxt = "—";
if (pm.length && prevF > 0) {
delta = Math.round(((curF - prevF) / prevF) * 1000) / 10;
deltaTxt = toneArrow(delta) + " " + Math.abs(delta) + "%";
}
var exec = curP > 0 ? Math.round((curF / curP) * 1000) / 10 : null;
var topG = null;
if (state.group === "all") {
var sh = groupShare();
var best = 0;
for (var s = 1; s < sh.length; s++) if (sh[s].sum > sh[best].sum) best = s;
topG = sh[best];
}
var lastMi = ms[ms.length - 1];
var lastVal = 0;
for (var g2 = 0; g2 < gs.length; g2++) { lastVal += FACT[gs[g2]][lastMi]; }
var prevMi = ms.length > 1 ? ms[ms.length - 2] : null;
var prevVal = 0;
if (prevMi !== null) {
for (var g3 = 0; g3 < gs.length; g3++) prevVal += FACT[gs[g3]][prevMi];
}
var mom = null, momTxt = "—";
if (prevMi !== null && prevVal > 0) {
mom = Math.round(((lastVal - prevVal) / prevVal) * 1000) / 10;
momTxt = toneArrow(mom) + " " + Math.abs(mom) + "%";
}
var unit = state.group === "all" ? "ед." : activeGroup().unit;
return [
{ label: "Продажи за период", value: fmt(curF), hint: unit + " · " + periodLabel(), data: "true" },
{ label: "К предыдущему периоду", value: deltaTxt, hint: pm.length ? "сравнение с предыдущим" : "—", data: toneCls(delta) },
{ label: "Выполнение плана", value: exec != null ? exec + "%" : "—", hint: "план " + fmt(curP) + " " + unit, data: exec != null ? (exec >= 97 ? "ok" : exec >= 92 ? "warn" : "risk") : "flat" },
{ label: "Лучшая группа", value: topG ? groupLabel(topG.id) : "—", hint: topG ? fmt(topG.sum) + " ед." : "выбрана одна группа", data: "flat" }
];
}
function renderKpis() {
var kpis = buildKpis();
return '<div class="kt-ai-kpi-strip" data-cards="true">' + kpis.map(function (k) {
var tone = "";
if (k.data === "up" || k.data === "down") tone = k.data;
return '<div class="kt-ai-kpi"><span class="label">' + esc(k.label) + '</span><span class="value"' + (tone ? ' data-tone="' + tone + '"' : "") + ">" + esc(k.value) + '</span><span class="hint">' + esc(k.hint) + "</span></div>";
}).join("") + "</div>";
}
function renderConclusion() {
var ms = periodMonths();
var gs = groupsOf();
var curF = 0, curP = 0;
for (var a = 0; a < gs.length; a++) {
for (var b = 0; b < ms.length; b++) { curF += FACT[gs[a]][ms[b]]; curP += PLAN[gs[a]][ms[b]]; }
}
var exec = curP > 0 ? Math.round((curF / curP) * 100) : 0;
var pm = prevMonths();
var deltaTxt = "";
if (pm.length && curF > 0) {
var prevF = 0;
for (var c = 0; c < gs.length; c++) {
for (var d = 0; d < pm.length; d++) prevF += FACT[gs[c]][pm[d]];
}
if (prevF > 0) {
var dl = Math.round(((curF - prevF) / prevF) * 10);
deltaTxt = dl >= 0 ? " ростом на " + dl + "%" : " снижением на " + Math.abs(dl) + "% к предыдущему периоду";
}
}
var lastMi = ms[ms.length - 1];
var lastVal = 0;
for (var g = 0; g < gs.length; g++) { lastVal += FACT[gs[g]][lastMi]; }
var topGroupTxt = "";
if (state.group === "all") {
var sh = groupShare();
var best = 0;
for (var s = 1; s < sh.length; s++) if (sh[s].sum > sh[best].sum) best = s;
topGroupTxt = "Больший вклад в период вносит «" + groupLabel(sh[best].id) + "» (" + sh[best].pct + "%). ";
}
return '<section class="kt-ai-conclusion"><div class="title">Вывод</div>' +
'<p class="text">За ' + esc(periodLabel().toLowerCase()) + " '26 розничные продажи составили " + fmt(curF) + " " + esc(unitWord()) + deltaTxt +
". Выполнение плана — " + exec + "%. Пик периода — " + MONTHS_FULL[lastMi].toLowerCase() + " (" + fmt(lastVal) + " " + esc(unitWord()) + "). " +
topGroupTxt + "Тренд по месяцам показан на графике: динамика относительно плана видна по расхождению линий.</p></section>";
}
function unitWord() {
return state.group === "all" ? "ед." : activeGroup().unit;
}
function renderTable() {
var ms = periodMonths().slice().sort(function (a, b) { return b - a; });
var gs = groupsOf();
var rows = [];
for (var i = 0; i < ms.length; i++) {
var mi = ms[i];
var f = 0, p = 0;
for (var g = 0; g < gs.length; g++) { f += FACT[gs[g]][mi]; p += PLAN[gs[g]][mi]; }
var exec = p > 0 ? Math.round((f / p) * 1000) / 10 : null;
rows.push({ month: MONTHS_FULL[mi], fact: f, plan: p, exec: exec, delta: null });
}
for (var r = 1; r < rows.length; r++) {
if (rows[r - 1].fact > 0) {
var d = Math.round(((rows[r].fact - rows[r - 1].fact) / rows[r - 1].fact) * 1000) / 10;
rows[r].delta = d;
}
}
var html = '<div class="kt-ai-card" style="padding:0;overflow:hidden"><div class="detail-head"><span class="kt-ai-label">Динамика по месяцам</span><span class="kt-ai-meta">' + esc(periodLabel()) + " · " + esc(state.group === "all" ? "все группы" : groupLabel(state.group)) + "</span></div>" +
'<div class="kt-ai-table-wrap"><table class="kt-ai-table" data-density="compact"><thead><tr>' +
"<th style=\"text-align:left\">Месяц</th><th style=\"text-align:left\">Группа</th><th style=\"text-align:right\">Факт</th><th style=\"text-align:right\">План</th><th style=\"text-align:right\">Исполнение, %</th><th style=\"text-align:right\">Δ к пред.</th>" +
"</tr></thead><tbody>";
for (var k2 = 0; k2 < rows.length; k2++) {
var row = rows[k2];
var pctTxt = row.exec != null ? row.exec + "%" : "—";
var pctTone = row.exec == null ? "flat" : row.exec >= 97 ? "ok" : row.exec >= 92 ? "warn" : "risk";
var dTxt = "—", dTone = "flat";
if (typeof row.delta === "number") {
dTxt = (row.delta >= 0 ? "+" : "−") + Math.abs(row.delta) + "%";
dTone = row.delta > 1.5 ? "up" : row.delta < -1.5 ? "down" : "flat";
}
html += "<tr>" +
'<td style="text-align:left" data-label="Месяц">' + esc(row.month) + "</td>" +
'<td style="text-align:left" data-label="Группа">' + esc(state.group === "all" ? "Все группы" : groupLabel(state.group)) + "</td>" +
'<td class="num" data-label="Факт">' + fmt(row.fact) + "</td>" +
'<td class="num" data-label="План">' + fmt(row.plan) + "</td>" +
'<td class="num" data-label="Исполнение, %">' + pctTxt + ' <span class="tone ' + pctTone + '"></span></td>' +
'<td class="num" data-label="Δ к пред."><span class="tone ' + dTone + '">' + dTxt + "</span></td>" +
"</tr>";
}
html += "</tbody></table></div></div>";
return html;
}
function renderView() {
var v = document.getElementById("view");
if (!v) return;
var h = "";
h += '<div class="kt-ai-banner" data-tone="info" style="margin-bottom:var(--kt-ai-stack-group)">' + esc(periodIntro()) + "</div>";
h += renderKpis();
h += '<div class="dash-grid">';
h += '<div class="dash-col-main">';
h += renderTrendChart();
h += renderTable();
h += renderConclusion();
h += "</div>";
h += '<div class="dash-col-side">';
h += renderDonut();
h += "</div>";
h += "</div>";
v.innerHTML = h;
}
function periodIntro() {
var name = state.group === "all" ? "по всем товарным группам" : "по группе «" + groupLabel(state.group) + "»";
return "Продажи " + periodLabel().toLowerCase() + " 2026 года " + name + ". Выберите период и группу товара — показатели, график и таблица пересчитаются.";
}
/* ---------- События ---------- */
function onPeriod() {
var el = document.getElementById("fPeriod");
if (el && el.value) {
state.period = el.value;
saveState();
var se = document.getElementById("fSearch");
if (se && state.q) { se.value = ""; state.q = ""; saveState(); }
renderView();
}
}
function onGroup() {
var el = document.getElementById("fGroup");
if (el && el.value) {
state.group = el.value;
saveState();
var se = document.getElementById("fSearch");
if (se && state.q) { se.value = ""; state.q = ""; saveState(); }
renderView();
}
}
function onSearch() {
var el = document.getElementById("fSearch");
if (el) {
state.q = el.value;
saveState();
applySearch();
}
}
function applySearch() {
var q = state.q.trim().toLowerCase();
var gSel = document.getElementById("fGroup");
if (!q) {
if (gSel && gSel.value === "all") return;
state.group = "all";
saveState();
if (gSel) gSel.value = "all";
renderView();
return;
}
for (var i = 0; i < GROUPS.length; i++) {
if (GROUPS[i].name.toLowerCase().indexOf(q) !== -1) {
if (gSel && gSel.value !== GROUPS[i].id) gSel.value = GROUPS[i].id;
if (state.group !== GROUPS[i].id) { state.group = GROUPS[i].id; saveState(); }
renderView();
return;
}
}
var el = document.getElementById("fSearch");
if (el) el.dataset.invalid = "true";
}
function onReset() {
state.period = "q3";
state.group = "all";
state.q = "";
saveState();
var p = document.getElementById("fPeriod"); if (p) p.value = "q3";
var g = document.getElementById("fGroup"); if (g) g.value = "all";
var s = document.getElementById("fSearch"); if (s) { s.value = ""; delete s.dataset.invalid; }
renderView();
}
function navTo(name) {
state.group = name;
saveState();
var g = document.getElementById("fGroup");
if (g) g.value = name;
renderView();
window.scrollTo(0, 0);
}
function init() {
loadState();
var p = document.getElementById("fPeriod"); if (p) p.value = state.period;
var g = document.getElementById("fGroup"); if (g) g.value = state.group;
var s = document.getElementById("fSearch"); if (s) s.value = state.q;
var pe = document.getElementById("fPeriod"); if (pe) pe.addEventListener("change", onPeriod);
var ge = document.getElementById("fGroup"); if (ge) ge.addEventListener("change", onGroup);
var se = document.getElementById("fSearch"); if (se) { se.addEventListener("input", onSearch); if (se.value) se.dataset.invalid = "false"; }
var rst = document.getElementById("btnReset"); if (rst) rst.addEventListener("click", onReset);
renderView();
}
document.addEventListener("DOMContentLoaded", init);