diff --git a/server/fishtest/static/js/application.js b/server/fishtest/static/js/application.js index 934585ec8..0876daef7 100644 --- a/server/fishtest/static/js/application.js +++ b/server/fishtest/static/js/application.js @@ -1,127 +1,86 @@ // https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript // https://stackoverflow.com/questions/40201533/sort-version-dotted-number-strings-in-javascript -const getCellValue = (tr, idx) => - tr.children[idx].dataset.diff || - tr.children[idx].innerText || - tr.children[idx].textContent; -const padDotVersion = (dn) => - dn - .split(".") - .map((n) => +n + 1000) - .join(""); -const padDotVersionStr = (dn) => dn.replace(/\d+/g, (n) => +n + 1000); +const getCellValue = (tr,idx)=>tr.children[idx].dataset.diff || tr.children[idx].innerText || tr.children[idx].textContent; +const padDotVersion = (dn)=>dn.split(".").map((n)=>+n + 1000).join(""); +const padDotVersionStr = (dn)=>dn.replace(/\d+/g, (n)=>+n + 1000); let p1, p2; -const comparer = (idx, asc) => (a, b) => - ((v1, v2) => - v1 !== "" && v2 !== "" && !isNaN(v1) && !isNaN(v2) - ? v1 - v2 - : v1 !== "" && v2 !== "" && !isNaN("0x" + v1) && !isNaN("0x" + v2) - ? parseInt(v1, 16) - parseInt(v2, 16) - : v1 !== "" && - v2 !== "" && - !isNaN((p1 = padDotVersion(v1))) && - !isNaN((p2 = padDotVersion(v2))) - ? p1 - p2 - : v1 !== "" && - v2 !== "" && - !isNaN(padDotVersion(v1.replace("clang++ ", "").replace("g++ ", ""))) && - !isNaN(padDotVersion(v2.replace("clang++ ", "").replace("g++ ", ""))) - ? padDotVersionStr(v1).toString().localeCompare(padDotVersionStr(v2)) - : v1.toString().localeCompare(v2))( - getCellValue(asc ? a : b, idx), - getCellValue(asc ? b : a, idx) - ); +const comparer = (idx,asc)=>(a,b)=>((v1,v2)=>v1 !== "" && v2 !== "" && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1 !== "" && v2 !== "" && !isNaN("0x" + v1) && !isNaN("0x" + v2) ? parseInt(v1, 16) - parseInt(v2, 16) : v1 !== "" && v2 !== "" && !isNaN((p1 = padDotVersion(v1))) && !isNaN((p2 = padDotVersion(v2))) ? p1 - p2 : v1 !== "" && v2 !== "" && !isNaN(padDotVersion(v1.replace("clang++ ", "").replace("g++ ", ""))) && !isNaN(padDotVersion(v2.replace("clang++ ", "").replace("g++ ", ""))) ? padDotVersionStr(v1).toString().localeCompare(padDotVersionStr(v2)) : v1.toString().localeCompare(v2))(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx)); -document.addEventListener("DOMContentLoaded", () => { - document.addEventListener("click", (e) => { - const { target } = e; - if (target.matches("th")) { - const th = target; - const table = th.closest("table"); - const body = table.querySelector("tbody"); - Array.from(body.querySelectorAll("tr")) - .sort( - comparer( - Array.from(th.parentNode.children).indexOf(th), - (this.asc = !this.asc) - ) - ) - .forEach((tr) => body.append(tr)); +document.addEventListener("DOMContentLoaded", ()=>{ + document.addEventListener("click", (e)=>{ + const {target} = e; + if (target.matches("th")) { + const th = target; + const table = th.closest("table"); + const body = table.querySelector("tbody"); + Array.from(body.querySelectorAll("tr")).sort(comparer(Array.from(th.parentNode.children).indexOf(th), (this.asc = !this.asc))).forEach((tr)=>body.append(tr)); + } } - }); + ); - // Click the sun/moon icons to change the color theme of the site - // hash calculated by browser for sub-resource integrity checks: - // https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity - const match = document.cookie.match( - new RegExp("(^| )" + "theme" + "=([^;]+)") - ); + // Click the sun/moon icons to change the color theme of the site + // hash calculated by browser for sub-resource integrity checks: + // https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity + const match = document.cookie.match(new RegExp("(^| )" + "theme" + "=([^;]+)")); - const setTheme = (theme) => { - if (theme === "dark") { - document.getElementById("sun").style.display = ""; - document.getElementById("moon").style.display = "none"; - const link = document.createElement("link"); - link["rel"] = "stylesheet"; - link["href"] = "/css/theme.dark.css?v=" + darkThemeHash; - link["integrity"] = "sha384-" + darkThemeHash; - link["crossOrigin"] = "anonymous"; - document.querySelector("head").append(link); - } else { - document.getElementById("sun").style.display = "none"; - document.getElementById("moon").style.display = ""; - document - .querySelector('head link[href*="/css/theme.dark.css"]') - ?.remove(); + const setTheme = (theme)=>{ + if (theme === "dark") { + document.getElementById("sun").style.display = ""; + document.getElementById("moon").style.display = "none"; + const link = document.createElement("link"); + link["rel"] = "stylesheet"; + link["href"] = "/css/theme.dark.css?v=" + darkThemeHash; + link["integrity"] = "sha384-" + darkThemeHash; + link["crossOrigin"] = "anonymous"; + document.querySelector("head").append(link); + } else { + document.getElementById("sun").style.display = "none"; + document.getElementById("moon").style.display = ""; + document.querySelector('head link[href*="/css/theme.dark.css"]')?.remove(); + } + // Remember the theme for 30 days + document.cookie = `theme=${theme};path=/;max-age=${30 * 24 * 60 * 60};SameSite=Lax;`; } - // Remember the theme for 30 days - document.cookie = `theme=${theme};path=/;max-age=${30 * 24 * 60 * 60};SameSite=Lax;`; - }; + ; - const getPreferredTheme = () => { - return window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light"; - }; + const getPreferredTheme = ()=>{ + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + ; - if (!match) { - setTheme(getPreferredTheme()); - } + if (!match) { + setTheme(getPreferredTheme()); + } - try { - window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", () => setTheme(getPreferredTheme())); - } catch (e) { - console.error(e); - } + try { + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", ()=>setTheme(getPreferredTheme())); + } catch (e) { + console.error(e); + } - document - .getElementById("sun") - .addEventListener("click", () => setTheme("light")); + document.getElementById("sun").addEventListener("click", ()=>setTheme("light")); - document - .getElementById("moon") - .addEventListener("click", () => setTheme("dark")); + document.getElementById("moon").addEventListener("click", ()=>setTheme("dark")); - // CSRF protection for links and forms - const csrfToken = document.querySelector("meta[name='csrf-token']")[ - "content" - ]; - document.getElementById("logout")?.addEventListener("click", (e) => { - e.preventDefault(); - fetch("/logout", { - method: "POST", - headers: { - "X-CSRF-Token": csrfToken, - }, - }).then(() => (window.location = "/")); - }); - document.querySelectorAll("form[method='POST']")?.forEach((form) => { - const input = document.createElement("input"); - input["type"] = "hidden"; - input["name"] = "csrf_token"; - input["value"] = csrfToken; - form.append(input); - }); -}); + // CSRF protection for links and forms + const csrfToken = document.querySelector("meta[name='csrf-token']")["content"]; + document.getElementById("logout")?.addEventListener("click", (e)=>{ + e.preventDefault(); + fetch("/logout", { + method: "POST", + headers: { + "X-CSRF-Token": csrfToken, + }, + }).then(()=>(window.location = "/")); + } + ); + document.querySelectorAll("form[method='POST']")?.forEach((form)=>{ + const input = document.createElement("input"); + input["type"] = "hidden"; + input["name"] = "csrf_token"; + input["value"] = csrfToken; + form.append(input); + } + ); +} +); diff --git a/server/fishtest/static/js/calc.js b/server/fishtest/static/js/calc.js index a11f267aa..0cf48eab0 100644 --- a/server/fishtest/static/js/calc.js +++ b/server/fishtest/static/js/calc.js @@ -1,265 +1,271 @@ "use strict"; -google.charts.load("current", { packages: ["corechart"] }); +google.charts.load("current", { + packages: ["corechart"] +}); let pass_chart = null; let expected_chart = null; let resize_timeout; -google.charts.setOnLoadCallback(function () { - const pass_prob_chart_div = document.getElementById("pass_prob_chart_div"); - pass_chart = new google.visualization.LineChart(pass_prob_chart_div); - pass_chart.div = pass_prob_chart_div; - pass_chart.loaded = false; - google.visualization.events.addListener(pass_chart, "ready", function () { - pass_chart.loaded = true; - }); +google.charts.setOnLoadCallback(function() { + const pass_prob_chart_div = document.getElementById("pass_prob_chart_div"); + pass_chart = new google.visualization.LineChart(pass_prob_chart_div); + pass_chart.div = pass_prob_chart_div; + pass_chart.loaded = false; + google.visualization.events.addListener(pass_chart, "ready", function() { + pass_chart.loaded = true; + }); - const expected_chart_div = document.getElementById("expected_chart_div"); - expected_chart = new google.visualization.LineChart(expected_chart_div); - expected_chart.div = expected_chart_div; - expected_chart.loaded = false; - google.visualization.events.addListener(expected_chart, "ready", function () { - expected_chart.loaded = true; - }); + const expected_chart_div = document.getElementById("expected_chart_div"); + expected_chart = new google.visualization.LineChart(expected_chart_div); + expected_chart.div = expected_chart_div; + expected_chart.loaded = false; + google.visualization.events.addListener(expected_chart, "ready", function() { + expected_chart.loaded = true; + }); - let mouse_screen = document.getElementById("mouse_screen"); - mouse_screen.addEventListener( - "click", - function (e) { - e.stopPropagation(); - }, - true - ); - mouse_screen.addEventListener( - "mouseover", - function (e) { - e.stopPropagation(); - }, - true - ); - mouse_screen.addEventListener("mousemove", handle_tooltips, true); - mouse_screen.addEventListener("mouseleave", handle_tooltips, true); - set_fields(); - draw_charts(); - window.onresize = function () { - clearTimeout(resize_timeout); - resize_timeout = setTimeout(draw_charts, 100); - }; + let mouse_screen = document.getElementById("mouse_screen"); + mouse_screen.addEventListener("click", function(e) { + e.stopPropagation(); + }, true); + mouse_screen.addEventListener("mouseover", function(e) { + e.stopPropagation(); + }, true); + mouse_screen.addEventListener("mousemove", handle_tooltips, true); + mouse_screen.addEventListener("mouseleave", handle_tooltips, true); + set_fields(); + draw_charts(); + window.onresize = function() { + clearTimeout(resize_timeout); + resize_timeout = setTimeout(draw_charts, 100); + } + ; }); function set_field_from_url(name, defaultValue) { - const value = url("?" + name); - let input = document.getElementById(name); - input.value = value !== null ? value : defaultValue; + const value = url("?" + name); + let input = document.getElementById(name); + input.value = value !== null ? value : defaultValue; } function set_fields() { - set_field_from_url("elo-model", "Normalized"); - set_field_from_url("elo-0", "-1"); - set_field_from_url("elo-1", "3"); - set_field_from_url("draw-ratio", "0.61"); - set_field_from_url("rms-bias", "0"); + set_field_from_url("elo-model", "Normalized"); + set_field_from_url("elo-0", "-1"); + set_field_from_url("elo-1", "3"); + set_field_from_url("draw-ratio", "0.61"); + set_field_from_url("rms-bias", "0"); } function draw_charts() { - const elo_model = document.getElementById("elo-model").value; - let elo0 = parseFloat(document.getElementById("elo-0").value); - let elo1 = parseFloat(document.getElementById("elo-1").value); - const draw_ratio = parseFloat(document.getElementById("draw-ratio").value); - const rms_bias = parseFloat(document.getElementById("rms-bias").value); - let val = ""; - let sprt; + const elo_model = document.getElementById("elo-model").value; + let elo0 = parseFloat(document.getElementById("elo-0").value); + let elo1 = parseFloat(document.getElementById("elo-1").value); + const draw_ratio = parseFloat(document.getElementById("draw-ratio").value); + const rms_bias = parseFloat(document.getElementById("rms-bias").value); + let val = ""; + let sprt; - if (isNaN(elo0) || isNaN(elo1) || isNaN(draw_ratio) || isNaN(rms_bias)) { - val = "Unreadable input."; - } else if (elo1 < elo0 + 0.5) { - val = "The difference between Elo1 and Elo0 must be at least 0.5."; - } else if (Math.abs(elo0) > 10 || Math.abs(elo1) > 10) { - val = "Elo values must be between -10 and 10."; - } else if (draw_ratio <= 0.0 || draw_ratio >= 1.0) { - val = "The draw ratio must be strictly between 0.0 and 1.0."; - } else if (rms_bias < 0) { - val = "The RMS bias must be positive."; - } else { - sprt = new Sprt(0.05, 0.05, elo0, elo1, draw_ratio, rms_bias, elo_model); - if (sprt.variance <= 0) { - val = "The draw ratio and the RMS bias are not compatible."; + if (isNaN(elo0) || isNaN(elo1) || isNaN(draw_ratio) || isNaN(rms_bias)) { + val = "Unreadable input."; + } else if (elo1 < elo0 + 0.5) { + val = "The difference between Elo1 and Elo0 must be at least 0.5."; + } else if (Math.abs(elo0) > 10 || Math.abs(elo1) > 10) { + val = "Elo values must be between -10 and 10."; + } else if (draw_ratio <= 0.0 || draw_ratio >= 1.0) { + val = "The draw ratio must be strictly between 0.0 and 1.0."; + } else if (rms_bias < 0) { + val = "The RMS bias must be positive."; + } else { + sprt = new Sprt(0.05,0.05,elo0,elo1,draw_ratio,rms_bias,elo_model); + if (sprt.variance <= 0) { + val = "The draw ratio and the RMS bias are not compatible."; + } + } + if (val != "") { + alert(val); + return; } - } - if (val != "") { - alert(val); - return; - } - elo0 = sprt.elo0; - elo1 = sprt.elo1; - pass_chart.loaded = false; - expected_chart.loaded = false; - let data_pass = [["Elo", { role: "annotation" }, "Pass Probability"]]; - let data_expected = [ - ["Elo", { role: "annotation" }, "Expected Number of Games"], - ]; - const d = elo1 - elo0; - const elo_start = Math.floor(elo0 - d / 3); - const elo_end = Math.ceil(elo1 + d / 3); - const N = elo_end - elo_start <= 5 ? 20 : 10; - // pseudo globals - pass_chart.elo_start = elo_start; - pass_chart.elo_end = elo_end; - const specials = [elo0, elo1]; - let anchors = []; - pass_chart.anchors = anchors; - for (let i = elo_start * N; i <= elo_end * N; i += 1) { - const elo = i / N; - anchors.push(elo); - const elo_next = (i + 1) / N; - const c = sprt.characteristics(elo); - data_pass.push([elo, null, { v: c[0], f: (c[0] * 100).toFixed(1) + "%" }]); - data_expected.push([ - elo, - null, - { v: c[1], f: (c[1] / 1000).toFixed(1) + "K" }, - ]); - for (const elo_ of specials) { - if (elo < elo_ && elo_next >= elo_) { - anchors.push(elo_); - const c_ = sprt.characteristics(elo_); - data_pass.push([ - elo_, - elo_, - { v: c_[0], f: (c_[0] * 100).toFixed(1) + "%" }, - ]); - data_expected.push([ - elo_, - elo_, - { v: c_[1], f: (c_[1] / 1000).toFixed(1) + "K" }, - ]); - } + elo0 = sprt.elo0; + elo1 = sprt.elo1; + pass_chart.loaded = false; + expected_chart.loaded = false; + let data_pass = [["Elo", { + role: "annotation" + }, "Pass Probability"]]; + let data_expected = [["Elo", { + role: "annotation" + }, "Expected Number of Games"], ]; + const d = elo1 - elo0; + const elo_start = Math.floor(elo0 - d / 3); + const elo_end = Math.ceil(elo1 + d / 3); + const N = elo_end - elo_start <= 5 ? 20 : 10; + // pseudo globals + pass_chart.elo_start = elo_start; + pass_chart.elo_end = elo_end; + const specials = [elo0, elo1]; + let anchors = []; + pass_chart.anchors = anchors; + for (let i = elo_start * N; i <= elo_end * N; i += 1) { + const elo = i / N; + anchors.push(elo); + const elo_next = (i + 1) / N; + const c = sprt.characteristics(elo); + data_pass.push([elo, null, { + v: c[0], + f: (c[0] * 100).toFixed(1) + "%" + }]); + data_expected.push([elo, null, { + v: c[1], + f: (c[1] / 1000).toFixed(1) + "K" + }, ]); + for (const elo_ of specials) { + if (elo < elo_ && elo_next >= elo_) { + anchors.push(elo_); + const c_ = sprt.characteristics(elo_); + data_pass.push([elo_, elo_, { + v: c_[0], + f: (c_[0] * 100).toFixed(1) + "%" + }, ]); + data_expected.push([elo_, elo_, { + v: c_[1], + f: (c_[1] / 1000).toFixed(1) + "K" + }, ]); + } + } } - } - const chart_text_style = { color: "#888" }; - const gridlines_style = { color: "#666" }; - const minor_gridlines_style = { color: "#aaa" }; - const title_text_style = { - color: "#999", - fontSize: 16, - bold: true, - italic: false, - }; + const chart_text_style = { + color: "#888" + }; + const gridlines_style = { + color: "#666" + }; + const minor_gridlines_style = { + color: "#aaa" + }; + const title_text_style = { + color: "#999", + fontSize: 16, + bold: true, + italic: false, + }; - let options = { - legend: { - position: "none", - }, - curveType: "function", - hAxis: { - title: "Logistic Elo", - titleTextStyle: title_text_style, - textStyle: chart_text_style, - gridlines: { - count: elo_end - elo_start, - color: "#666", - }, - minorGridlines: minor_gridlines_style, - }, - vAxis: { - title: "Pass Probability", - titleTextStyle: title_text_style, - textStyle: chart_text_style, - gridlines: gridlines_style, - minorGridlines: minor_gridlines_style, - format: "percent", - }, - tooltip: { - trigger: "selection", - }, - backgroundColor: { - fill: "transparent", - }, - chartArea: { - left: "15%", - top: "5%", - width: "80%", - height: "80%", - }, - annotations: { - style: "line", - stem: { color: "orange" }, - textStyle: title_text_style, - }, - }; - let data_table = google.visualization.arrayToDataTable(data_pass); - pass_chart.draw(data_table, options); - options.vAxis = { - title: "Expected Number of Games", - titleTextStyle: title_text_style, - textStyle: chart_text_style, - gridlines: gridlines_style, - minorGridlines: minor_gridlines_style, - format: "short", - }; - data_table = google.visualization.arrayToDataTable(data_expected); - expected_chart.draw(data_table, options); + let options = { + legend: { + position: "none", + }, + curveType: "function", + hAxis: { + title: "Logistic Elo", + titleTextStyle: title_text_style, + textStyle: chart_text_style, + gridlines: { + count: elo_end - elo_start, + color: "#666", + }, + minorGridlines: minor_gridlines_style, + }, + vAxis: { + title: "Pass Probability", + titleTextStyle: title_text_style, + textStyle: chart_text_style, + gridlines: gridlines_style, + minorGridlines: minor_gridlines_style, + format: "percent", + }, + tooltip: { + trigger: "selection", + }, + backgroundColor: { + fill: "transparent", + }, + chartArea: { + left: "15%", + top: "5%", + width: "80%", + height: "80%", + }, + annotations: { + style: "line", + stem: { + color: "orange" + }, + textStyle: title_text_style, + }, + }; + let data_table = google.visualization.arrayToDataTable(data_pass); + pass_chart.draw(data_table, options); + options.vAxis = { + title: "Expected Number of Games", + titleTextStyle: title_text_style, + textStyle: chart_text_style, + gridlines: gridlines_style, + minorGridlines: minor_gridlines_style, + format: "short", + }; + data_table = google.visualization.arrayToDataTable(data_expected); + expected_chart.draw(data_table, options); } function ready() { - return ( - pass_chart != null && - pass_chart.loaded && - expected_chart != null && - expected_chart.loaded - ); + return (pass_chart != null && pass_chart.loaded && expected_chart != null && expected_chart.loaded); } function contains(rect, x, y) { - return x >= rect.left && x <= rect.right && y <= rect.bottom && y >= rect.top; + return x >= rect.left && x <= rect.right && y <= rect.bottom && y >= rect.top; } function handle_tooltips(e) { - // generic mouse events handler - e.stopPropagation(); - if (!ready()) { - return; - } - const x = e.clientX; - const y = e.clientY; - let rect; - let rect_pass = pass_chart.div.getBoundingClientRect(); - let rect_expected = expected_chart.div.getBoundingClientRect(); - let chart; - if (contains(rect_pass, x, y)) { - chart = pass_chart; - rect = rect_pass; - } else if (contains(rect_expected, x, y)) { - chart = expected_chart; - rect = rect_expected; - } else { - pass_chart.setSelection([]); - expected_chart.setSelection([]); - return; - } - const elo = chart.getChartLayoutInterface().getHAxisValue(x - rect.left); - const anchors = pass_chart.anchors; - let row; - let last_dist = null; - for (row = 0; row < anchors.length; row++) { - const dist = Math.abs(anchors[row] - elo); - if (last_dist != null && dist > last_dist) { - break; + // generic mouse events handler + e.stopPropagation(); + if (!ready()) { + return; + } + const x = e.clientX; + const y = e.clientY; + let rect; + let rect_pass = pass_chart.div.getBoundingClientRect(); + let rect_expected = expected_chart.div.getBoundingClientRect(); + let chart; + if (contains(rect_pass, x, y)) { + chart = pass_chart; + rect = rect_pass; + } else if (contains(rect_expected, x, y)) { + chart = expected_chart; + rect = rect_expected; + } else { + pass_chart.setSelection([]); + expected_chart.setSelection([]); + return; + } + const elo = chart.getChartLayoutInterface().getHAxisValue(x - rect.left); + const anchors = pass_chart.anchors; + let row; + let last_dist = null; + for (row = 0; row < anchors.length; row++) { + const dist = Math.abs(anchors[row] - elo); + if (last_dist != null && dist > last_dist) { + break; + } else { + last_dist = dist; + } + } + row--; + const elo_start = pass_chart.elo_start; + const elo_end = pass_chart.elo_end; + const d = (elo_end - elo_start) / 20; + if (elo >= elo_start - d && elo <= elo_end + d) { + pass_chart.setSelection([{ + row: row, + column: 2 + }]); + expected_chart.setSelection([{ + row: row, + column: 2 + }]); } else { - last_dist = dist; + pass_chart.setSelection([]); + expected_chart.setSelection([]); } - } - row--; - const elo_start = pass_chart.elo_start; - const elo_end = pass_chart.elo_end; - const d = (elo_end - elo_start) / 20; - if (elo >= elo_start - d && elo <= elo_end + d) { - pass_chart.setSelection([{ row: row, column: 2 }]); - expected_chart.setSelection([{ row: row, column: 2 }]); - } else { - pass_chart.setSelection([]); - expected_chart.setSelection([]); - } } diff --git a/server/fishtest/static/js/live_elo.js b/server/fishtest/static/js/live_elo.js index 48e516804..d510e9b03 100644 --- a/server/fishtest/static/js/live_elo.js +++ b/server/fishtest/static/js/live_elo.js @@ -1,191 +1,163 @@ "use strict"; function supportsNotifications() { - if ( - // Safari on iOS doesn't support them - "Notification" in window && - // Chrome and Opera on Android don't support them - !( - navigator.userAgent.match(/Android/i) && - navigator.userAgent.match(/Chrome/i) - ) - ) - return true; - return false; + if (// Safari on iOS doesn't support them + "Notification"in window && // Chrome and Opera on Android don't support them + !(navigator.userAgent.match(/Android/i) && navigator.userAgent.match(/Chrome/i))) + return true; + return false; } if (supportsNotifications() && Notification.permission === "default") { - document.getElementById("notificationsAlert").classList.remove("d-none"); + document.getElementById("notificationsAlert").classList.remove("d-none"); } function notify(tag, state, elo) { - const notification = new Notification(`Test ${tag} ${state}!`, { - body: `Elo: ${elo > 0 ? "+" : ""}${elo.toFixed(2)}`, - requireInteraction: true, - icon: "https://tests.stockfishchess.org/img/stockfish.png", - }); - notification.onclick = () => { - window.parent.parent.focus(); - }; + const notification = new Notification(`Test ${tag} ${state}!`,{ + body: `Elo: ${elo > 0 ? "+" : ""}${elo.toFixed(2)}`, + requireInteraction: true, + icon: "https://tests.stockfishchess.org/img/stockfish.png", + }); + notification.onclick = ()=>{ + window.parent.parent.focus(); + } + ; } -google.charts.load("current", { packages: ["gauge"] }); +google.charts.load("current", { + packages: ["gauge"] +}); let LOS_chart = null; let LLR_chart = null; let ELO_chart = null; -google.charts.setOnLoadCallback(function () { - LOS_chart = new google.visualization.Gauge( - document.getElementById("LOS_chart_div") - ); - LLR_chart = new google.visualization.Gauge( - document.getElementById("LLR_chart_div") - ); - ELO_chart = new google.visualization.Gauge( - document.getElementById("ELO_chart_div") - ); - clear_gauges(); - follow_live(test_id); +google.charts.setOnLoadCallback(function() { + LOS_chart = new google.visualization.Gauge(document.getElementById("LOS_chart_div")); + LLR_chart = new google.visualization.Gauge(document.getElementById("LLR_chart_div")); + ELO_chart = new google.visualization.Gauge(document.getElementById("ELO_chart_div")); + clear_gauges(); + follow_live(test_id); }); function collect(m) { - const sprt = m.args.sprt; - const results = m.results; - const ret = m.elo; - ret.alpha = sprt.alpha; - ret.beta = sprt.beta; - ret.elo_raw0 = sprt.elo0; - ret.elo_raw1 = sprt.elo1; - ret.elo_model = sprt.elo_model; - ret.W = results.wins; - ret.D = results.draws; - ret.L = results.losses; - ret.ci_lower = ret.ci[0]; - ret.ci_upper = ret.ci[1]; - ret.games = ret.W + ret.D + ret.L; - ret.p = 0.05; - return ret; + const sprt = m.args.sprt; + const results = m.results; + const ret = m.elo; + ret.alpha = sprt.alpha; + ret.beta = sprt.beta; + ret.elo_raw0 = sprt.elo0; + ret.elo_raw1 = sprt.elo1; + ret.elo_model = sprt.elo_model; + ret.W = results.wins; + ret.D = results.draws; + ret.L = results.losses; + ret.ci_lower = ret.ci[0]; + ret.ci_upper = ret.ci[1]; + ret.games = ret.W + ret.D + ret.L; + ret.p = 0.05; + return ret; } function set_gauges(LLR, a, b, LOS, elo, ci_lower, ci_upper) { - if (!set_gauges.last_elo) { - set_gauges.last_elo = 0; - } - const LOS_chart_data = google.visualization.arrayToDataTable([ - ["Label", "Value"], - ["LOS", Math.round(1000 * LOS) / 10], - ]); - const LOS_chart_options = { - width: 500, - height: 150, - greenFrom: 95, - greenTo: 100, - yellowFrom: 5, - yellowTo: 95, - redFrom: 0, - redTo: 5, - minorTicks: 5, - }; - LOS_chart.draw(LOS_chart_data, LOS_chart_options); - - const LLR_chart_data = google.visualization.arrayToDataTable([ - ["Label", "Value"], - ["LLR", Math.round(100 * LLR) / 100], - ]); - a = Math.round(100 * a) / 100; - b = Math.round(100 * b) / 100; - const LLR_chart_options = { - width: 500, - height: 150, - greenFrom: b, - greenTo: b * 1.04, - redFrom: a * 1.04, - redTo: a, - yellowFrom: a, - yellowTo: b, - max: b, - min: a, - minorTicks: 3, - }; - LLR_chart.draw(LLR_chart_data, LLR_chart_options); - - const ELO_chart_data = google.visualization.arrayToDataTable([ - ["Label", "Value"], - ["Elo", set_gauges.last_elo], - ]); - const ELO_chart_options = { - width: 500, - height: 150, - max: 4, - min: -4, - minorTicks: 4, - }; - if (ci_lower < 0 && ci_upper > 0) { - ELO_chart_options.redFrom = ci_lower; - ELO_chart_options.redTo = 0; - ELO_chart_options.yellowFrom = 0; - ELO_chart_options.yellowTo = 0; - ELO_chart_options.greenFrom = 0; - ELO_chart_options.greenTo = ci_upper; - } else if (ci_lower >= 0) { - ELO_chart_options.redFrom = ci_lower; - ELO_chart_options.redTo = ci_lower; - ELO_chart_options.yellowFrom = ci_lower; - ELO_chart_options.yellowTo = ci_lower; - ELO_chart_options.greenFrom = ci_lower; - ELO_chart_options.greenTo = ci_upper; - } else if (ci_upper <= 0) { - ELO_chart_options.redFrom = ci_lower; - ELO_chart_options.redTo = ci_upper; - ELO_chart_options.yellowFrom = ci_upper; - ELO_chart_options.yellowTo = ci_upper; - ELO_chart_options.greenFrom = ci_upper; - ELO_chart_options.greenTo = ci_upper; - } - ELO_chart.draw(ELO_chart_data, ELO_chart_options); - elo = Math.round(100 * elo) / 100; - ELO_chart_data.setValue(0, 1, elo); - ELO_chart.draw(ELO_chart_data, ELO_chart_options); // 2nd draw to get animation - set_gauges.last_elo = elo; + if (!set_gauges.last_elo) { + set_gauges.last_elo = 0; + } + const LOS_chart_data = google.visualization.arrayToDataTable([["Label", "Value"], ["LOS", Math.round(1000 * LOS) / 10], ]); + const LOS_chart_options = { + width: 500, + height: 150, + greenFrom: 95, + greenTo: 100, + yellowFrom: 5, + yellowTo: 95, + redFrom: 0, + redTo: 5, + minorTicks: 5, + }; + LOS_chart.draw(LOS_chart_data, LOS_chart_options); + + const LLR_chart_data = google.visualization.arrayToDataTable([["Label", "Value"], ["LLR", Math.round(100 * LLR) / 100], ]); + a = Math.round(100 * a) / 100; + b = Math.round(100 * b) / 100; + const LLR_chart_options = { + width: 500, + height: 150, + greenFrom: b, + greenTo: b * 1.04, + redFrom: a * 1.04, + redTo: a, + yellowFrom: a, + yellowTo: b, + max: b, + min: a, + minorTicks: 3, + }; + LLR_chart.draw(LLR_chart_data, LLR_chart_options); + + const ELO_chart_data = google.visualization.arrayToDataTable([["Label", "Value"], ["Elo", set_gauges.last_elo], ]); + const ELO_chart_options = { + width: 500, + height: 150, + max: 4, + min: -4, + minorTicks: 4, + }; + if (ci_lower < 0 && ci_upper > 0) { + ELO_chart_options.redFrom = ci_lower; + ELO_chart_options.redTo = 0; + ELO_chart_options.yellowFrom = 0; + ELO_chart_options.yellowTo = 0; + ELO_chart_options.greenFrom = 0; + ELO_chart_options.greenTo = ci_upper; + } else if (ci_lower >= 0) { + ELO_chart_options.redFrom = ci_lower; + ELO_chart_options.redTo = ci_lower; + ELO_chart_options.yellowFrom = ci_lower; + ELO_chart_options.yellowTo = ci_lower; + ELO_chart_options.greenFrom = ci_lower; + ELO_chart_options.greenTo = ci_upper; + } else if (ci_upper <= 0) { + ELO_chart_options.redFrom = ci_lower; + ELO_chart_options.redTo = ci_upper; + ELO_chart_options.yellowFrom = ci_upper; + ELO_chart_options.yellowTo = ci_upper; + ELO_chart_options.greenFrom = ci_upper; + ELO_chart_options.greenTo = ci_upper; + } + ELO_chart.draw(ELO_chart_data, ELO_chart_options); + elo = Math.round(100 * elo) / 100; + ELO_chart_data.setValue(0, 1, elo); + ELO_chart.draw(ELO_chart_data, ELO_chart_options); + // 2nd draw to get animation + set_gauges.last_elo = elo; } function clear_gauges() { - set_gauges(0, -2.94, 2.94, 0.5, 0, 0, 0); + set_gauges(0, -2.94, 2.94, 0.5, 0, 0, 0); } function display_data(items) { - const j = collect(items); - - if ( - // Only notify if the test has a state (accepted or rejected) - items.args.sprt.state && - // Only notify if there is already text in LLR (the test wasn't just opened) - document.getElementById("LLR").textContent !== "" && - supportsNotifications() && - Notification.permission === "granted" - ) { - notify(items.args.new_tag, items.args.sprt.state, j.elo); - } - - document.getElementById("data").style.visibility = "visible"; - - document.getElementById( - "commit" - ).href = `${items.args.tests_repo}/compare/${items.args.resolved_base}...${items.args.resolved_new}`; - document.getElementById( - "commit" - ).textContent = `${items.args.new_tag} (${items.args.msg_new})`; - - document.getElementById("info").textContent = items.args.info; - - document.getElementById( - "username" - ).href = `/tests/user/${items.args.username}`; - document.getElementById("username").textContent = items.args.username; - - document.getElementById("tc").textContent = items.args.tc; - - document.getElementById("sprt").textContent = ` + const j = collect(items); + + if (// Only notify if the test has a state (accepted or rejected) + items.args.sprt.state && // Only notify if there is already text in LLR (the test wasn't just opened) + document.getElementById("LLR").textContent !== "" && supportsNotifications() && Notification.permission === "granted") { + notify(items.args.new_tag, items.args.sprt.state, j.elo); + } + + document.getElementById("data").style.visibility = "visible"; + + document.getElementById("commit").href = `${items.args.tests_repo}/compare/${items.args.resolved_base}...${items.args.resolved_new}`; + document.getElementById("commit").textContent = `${items.args.new_tag} (${items.args.msg_new})`; + + document.getElementById("info").textContent = items.args.info; + + document.getElementById("username").href = `/tests/user/${items.args.username}`; + document.getElementById("username").textContent = items.args.username; + + document.getElementById("tc").textContent = items.args.tc; + + document.getElementById("sprt").textContent = ` elo0:\xA0${j.elo_raw0.toFixed(2)}\xA0 alpha:\xA0${j.alpha.toFixed(2)}\xA0 elo1:\xA0${j.elo_raw1.toFixed(2)}\xA0 @@ -193,53 +165,54 @@ function display_data(items) { (${j.elo_model}) `; - document.getElementById("LLR").textContent = ` + document.getElementById("LLR").textContent = ` ${j.LLR.toFixed(2)} [${j.a.toFixed(2)},${j.b.toFixed(2)}] ${items.args.sprt.state ? `(${items.args.sprt.state})` : ""} `; - document.getElementById("elo").textContent = ` + document.getElementById("elo").textContent = ` ${j.elo.toFixed(2)} [${j.ci_lower.toFixed(2)},${j.ci_upper.toFixed(2)}] (${100 * (1 - j.p).toFixed(2)}%) `; - document.getElementById("LOS").textContent = `${(100 * j.LOS).toFixed(1)}%`; + document.getElementById("LOS").textContent = `${(100 * j.LOS).toFixed(1)}%`; - document.getElementById("games").textContent = ` + document.getElementById("games").textContent = ` ${j.games} [w:${((100 * Math.round(j.W)) / (j.games + 0.001)).toFixed(1)}%, l:${((100 * Math.round(j.L)) / (j.games + 0.001)).toFixed(1)}%, d:${((100 * Math.round(j.D)) / (j.games + 0.001)).toFixed(1)}%] `; - set_gauges(j.LLR, j.a, j.b, j.LOS, j.elo, j.ci_lower, j.ci_upper); + set_gauges(j.LLR, j.a, j.b, j.LOS, j.elo, j.ci_lower, j.ci_upper); } // Main worker. function follow_live(test_id) { - if (follow_live.timer_once === undefined) { - follow_live.timer_once = null; - } - if (follow_live.timer_once != null) { - clearTimeout(follow_live.timer_once); - follow_live.timer_once = null; - } - let xhttp = new XMLHttpRequest(); - const timestamp = new Date().getTime(); - xhttp.open("GET", "/api/get_elo/" + test_id + "?" + timestamp, true); - xhttp.onreadystatechange = function () { - if (this.readyState == 4) { - if (this.status == 200) { - const m = JSON.parse(this.responseText); - if (!m.args.sprt.state) - follow_live.timer_once = setTimeout(follow_live, 20000, test_id); - display_data(m); - } else { - follow_live.timer_once = setTimeout(follow_live, 20000, test_id); - } + if (follow_live.timer_once === undefined) { + follow_live.timer_once = null; + } + if (follow_live.timer_once != null) { + clearTimeout(follow_live.timer_once); + follow_live.timer_once = null; + } + let xhttp = new XMLHttpRequest(); + const timestamp = new Date().getTime(); + xhttp.open("GET", "/api/get_elo/" + test_id + "?" + timestamp, true); + xhttp.onreadystatechange = function() { + if (this.readyState == 4) { + if (this.status == 200) { + const m = JSON.parse(this.responseText); + if (!m.args.sprt.state) + follow_live.timer_once = setTimeout(follow_live, 20000, test_id); + display_data(m); + } else { + follow_live.timer_once = setTimeout(follow_live, 20000, test_id); + } + } } - }; - xhttp.send(); + ; + xhttp.send(); } diff --git a/server/fishtest/static/js/sprt.js b/server/fishtest/static/js/sprt.js index 783ffb5b6..0f0769c77 100644 --- a/server/fishtest/static/js/sprt.js +++ b/server/fishtest/static/js/sprt.js @@ -6,59 +6,59 @@ const nelo_divided_by_nt = 800 / Math.log(10); function L(x) { - return 1 / (1 + Math.pow(10, -x / 400)); + return 1 / (1 + Math.pow(10, -x / 400)); } function Linv(x) { - return -400 * Math.log10(1 / x - 1); + return -400 * Math.log10(1 / x - 1); } function PT(LA, LB, h) { - // Universal functions - let P, T; - if (Math.abs(h * (LA - LB)) < 1e-6) { - // avoid division by zero - P = -LA / (LB - LA); - T = -LA * LB; - } else { - const exp_a = Math.exp(-h * LA); - const exp_b = Math.exp(-h * LB); - P = (1 - exp_a) / (exp_b - exp_a); - T = (2 / h) * (LB * P + LA * (1 - P)); - } - return [P, T]; + // Universal functions + let P, T; + if (Math.abs(h * (LA - LB)) < 1e-6) { + // avoid division by zero + P = -LA / (LB - LA); + T = -LA * LB; + } else { + const exp_a = Math.exp(-h * LA); + const exp_b = Math.exp(-h * LB); + P = (1 - exp_a) / (exp_b - exp_a); + T = (2 / h) * (LB * P + LA * (1 - P)); + } + return [P, T]; } function Sprt(alpha, beta, elo0, elo1, draw_ratio, rms_bias, elo_model) { - const rms_bias_score = L(rms_bias) - 0.5; - const variance3 = (1 - draw_ratio) / 4.0; - this.variance = variance3 - Math.pow(rms_bias_score, 2); - if (this.variance <= 0) { - return; - } - if (elo_model == "Logistic") { - this.elo0 = elo0; - this.elo1 = elo1; - this.score0 = L(elo0); - this.score1 = L(elo1); - } else { - // Assume "Normalized" - const nt0 = elo0 / nelo_divided_by_nt; - const nt1 = elo1 / nelo_divided_by_nt; - const sigma = Math.sqrt(this.variance); - this.score0 = nt0 * sigma + 0.5; - this.score1 = nt1 * sigma + 0.5; - this.elo0 = Linv(this.score0); - this.elo1 = Linv(this.score1); - } - this.w2 = Math.pow(this.score1 - this.score0, 2) / this.variance; - this.LA = Math.log(beta / (1 - alpha)); - this.LB = Math.log((1 - beta) / alpha); - this.characteristics = function (elo) { - const score = L(elo); - const h = - (2 * score - (this.score0 + this.score1)) / (this.score1 - this.score0); - const PT_ = PT(this.LA, this.LB, h); - return [PT_[0], PT_[1] / this.w2]; - }; + const rms_bias_score = L(rms_bias) - 0.5; + const variance3 = (1 - draw_ratio) / 4.0; + this.variance = variance3 - Math.pow(rms_bias_score, 2); + if (this.variance <= 0) { + return; + } + if (elo_model == "Logistic") { + this.elo0 = elo0; + this.elo1 = elo1; + this.score0 = L(elo0); + this.score1 = L(elo1); + } else { + // Assume "Normalized" + const nt0 = elo0 / nelo_divided_by_nt; + const nt1 = elo1 / nelo_divided_by_nt; + const sigma = Math.sqrt(this.variance); + this.score0 = nt0 * sigma + 0.5; + this.score1 = nt1 * sigma + 0.5; + this.elo0 = Linv(this.score0); + this.elo1 = Linv(this.score1); + } + this.w2 = Math.pow(this.score1 - this.score0, 2) / this.variance; + this.LA = Math.log(beta / (1 - alpha)); + this.LB = Math.log((1 - beta) / alpha); + this.characteristics = function(elo) { + const score = L(elo); + const h = (2 * score - (this.score0 + this.score1)) / (this.score1 - this.score0); + const PT_ = PT(this.LA, this.LB, h); + return [PT_[0], PT_[1] / this.w2]; + } + ; } diff --git a/server/fishtest/static/js/spsa.js b/server/fishtest/static/js/spsa.js index 914c0c784..88de99c3c 100644 --- a/server/fishtest/static/js/spsa.js +++ b/server/fishtest/static/js/spsa.js @@ -1,332 +1,265 @@ -(function () { - let raw = [], - chart_object, - chart_data, - data_cache = [], - smoothing_factor = 0, - smoothing_max = 20, - columns = [], - viewAll = false; +(function() { + let raw = [], chart_object, chart_data, data_cache = [], smoothing_factor = 0, smoothing_max = 20, columns = [], viewAll = false; - const chart_colors = [ - "#3366cc", - "#dc3912", - "#ff9900", - "#109618", - "#990099", - "#0099c6", - "#dd4477", - "#66aa00", - "#b82e2e", - "#316395", - "#994499", - "#22aa99", - "#aaaa11", - "#6633cc", - "#e67300", - "#8b0707", - "#651067", - "#329262", - "#5574a6", - "#3b3eac", - "#b77322", - "#16d620", - "#b91383", - "#f4359e", - "#9c5935", - "#a9c413", - "#2a778d", - "#668d1c", - "#bea413", - "#0c5922", - "#743411", - "#3366cc", - "#dc3912", - "#ff9900", - "#109618", - "#990099", - "#0099c6", - "#dd4477", - "#66aa00", - "#b82e2e", - "#316395", - "#994499", - "#22aa99", - "#aaaa11", - "#6633cc", - "#e67300", - "#8b0707", - "#651067", - "#329262", - "#5574a6", - "#3b3eac", - "#b77322", - "#16d620", - "#b91383", - "#f4359e", - "#9c5935", - "#a9c413", - "#2a778d", - "#668d1c", - "#bea413", - "#0c5922", - "#743411", - ]; + const chart_colors = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac", "#b77322", "#16d620", "#b91383", "#f4359e", "#9c5935", "#a9c413", "#2a778d", "#668d1c", "#bea413", "#0c5922", "#743411", "#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac", "#b77322", "#16d620", "#b91383", "#f4359e", "#9c5935", "#a9c413", "#2a778d", "#668d1c", "#bea413", "#0c5922", "#743411", ]; - const chart_invisible_color = "#ccc"; - const chart_text_style = { color: "#888" }; - const gridlines_style = { color: "#666" }; - const minor_gridlines_style = { color: "#ccc" }; + const chart_invisible_color = "#ccc"; + const chart_text_style = { + color: "#888" + }; + const gridlines_style = { + color: "#666" + }; + const minor_gridlines_style = { + color: "#ccc" + }; - let chart_options = { - backgroundColor: { - fill: "transparent", - }, - curveType: "function", - chartArea: { - width: "800", - height: "450", - left: 40, - top: 20, - }, - width: 1000, - height: 500, - hAxis: { - format: "percent", - textStyle: chart_text_style, - gridlines: gridlines_style, - minorGridlines: minor_gridlines_style, - }, - vAxis: { - viewWindowMode: "maximized", - textStyle: chart_text_style, - gridlines: gridlines_style, - minorGridlines: minor_gridlines_style, - }, - legend: { - position: "right", - textStyle: chart_text_style, - }, - colors: chart_colors.slice(0), - seriesType: "line", - }; + let chart_options = { + backgroundColor: { + fill: "transparent", + }, + curveType: "function", + chartArea: { + width: "800", + height: "450", + left: 40, + top: 20, + }, + width: 1000, + height: 500, + hAxis: { + format: "percent", + textStyle: chart_text_style, + gridlines: gridlines_style, + minorGridlines: minor_gridlines_style, + }, + vAxis: { + viewWindowMode: "maximized", + textStyle: chart_text_style, + gridlines: gridlines_style, + minorGridlines: minor_gridlines_style, + }, + legend: { + position: "right", + textStyle: chart_text_style, + }, + colors: chart_colors.slice(0), + seriesType: "line", + }; - function gaussian_kernel_regression(y, h) { - if (!h) return y; + function gaussian_kernel_regression(y, h) { + if (!h) + return y; - let rf = []; - for (let i = 0; i < y.length; i++) { - let yt = 0; - let zt = 0; - for (let j = 0; j < y.length; j++) { - const p = (i - j) / h; - const z = Math.exp((p * -1 * p) / 2); - zt += z; - yt += z * y[j]; - } - rf.push(yt / zt); + let rf = []; + for (let i = 0; i < y.length; i++) { + let yt = 0; + let zt = 0; + for (let j = 0; j < y.length; j++) { + const p = (i - j) / h; + const z = Math.exp((p * -1 * p) / 2); + zt += z; + yt += z * y[j]; + } + rf.push(yt / zt); + } + return rf; } - return rf; - } - function smooth_data(b) { - const spsa_params = spsa_data.params; - const spsa_history = spsa_data.param_history; - const spsa_iter_ratio = Math.min(spsa_data.iter / spsa_data.num_iter, 1); + function smooth_data(b) { + const spsa_params = spsa_data.params; + const spsa_history = spsa_data.param_history; + const spsa_iter_ratio = Math.min(spsa_data.iter / spsa_data.num_iter, 1); - //cache the raw data - if (!raw.length) { - for (let j = 0; j < spsa_params.length; j++) raw.push([]); - for (let i = 0; i < spsa_history.length; i++) { - for (let j = 0; j < spsa_params.length; j++) { - raw[j].push(spsa_history[i][j].theta); + //cache the raw data + if (!raw.length) { + for (let j = 0; j < spsa_params.length; j++) + raw.push([]); + for (let i = 0; i < spsa_history.length; i++) { + for (let j = 0; j < spsa_params.length; j++) { + raw[j].push(spsa_history[i][j].theta); + } + } } - } - } - //cache data table to avoid recomputing the smoothed graph - if (!data_cache[b]) { - let dt = new google.visualization.DataTable(); - dt.addColumn("number", "Iteration"); - for (let i = 0; i < spsa_params.length; i++) { - dt.addColumn("number", spsa_params[i].name); - } - // adjust the bandwidth for tests with samples != 101 - const h = b * ((spsa_history.length - 1) / (spsa_iter_ratio * 100)); - let d = []; - for (let j = 0; j < spsa_params.length; j++) { - d.push(gaussian_kernel_regression(raw[j], h)); - } - let googleformat = []; - for (let i = 0; i < spsa_history.length; i++) { - let c = [(i / (spsa_history.length - 1)) * spsa_iter_ratio]; - for (let j = 0; j < spsa_params.length; j++) { - c.push(d[j][i]); + //cache data table to avoid recomputing the smoothed graph + if (!data_cache[b]) { + let dt = new google.visualization.DataTable(); + dt.addColumn("number", "Iteration"); + for (let i = 0; i < spsa_params.length; i++) { + dt.addColumn("number", spsa_params[i].name); + } + // adjust the bandwidth for tests with samples != 101 + const h = b * ((spsa_history.length - 1) / (spsa_iter_ratio * 100)); + let d = []; + for (let j = 0; j < spsa_params.length; j++) { + d.push(gaussian_kernel_regression(raw[j], h)); + } + let googleformat = []; + for (let i = 0; i < spsa_history.length; i++) { + let c = [(i / (spsa_history.length - 1)) * spsa_iter_ratio]; + for (let j = 0; j < spsa_params.length; j++) { + c.push(d[j][i]); + } + googleformat.push(c); + } + dt.addRows(googleformat); + data_cache[b] = dt; } - googleformat.push(c); - } - dt.addRows(googleformat); - data_cache[b] = dt; + chart_data = data_cache[b]; + redraw(true); } - chart_data = data_cache[b]; - redraw(true); - } - function redraw(animate) { - chart_options.animation = animate ? { duration: 800, easing: "out" } : {}; - let view = new google.visualization.DataView(chart_data); - view.setColumns(columns); - chart_object.draw(view, chart_options); - } + function redraw(animate) { + chart_options.animation = animate ? { + duration: 800, + easing: "out" + } : {}; + let view = new google.visualization.DataView(chart_data); + view.setColumns(columns); + chart_object.draw(view, chart_options); + } - function update_column_visibility(col, visibility) { - if (!visibility) { - columns[col] = { - label: chart_data.getColumnLabel(col), - type: chart_data.getColumnType(col), - calc: function () { - return null; - }, - }; - chart_options.colors[col - 1] = chart_invisible_color; - } else { - columns[col] = col; - chart_options.colors[col - 1] = - chart_colors[(col - 1) % chart_colors.length]; + function update_column_visibility(col, visibility) { + if (!visibility) { + columns[col] = { + label: chart_data.getColumnLabel(col), + type: chart_data.getColumnType(col), + calc: function() { + return null; + }, + }; + chart_options.colors[col - 1] = chart_invisible_color; + } else { + columns[col] = col; + chart_options.colors[col - 1] = chart_colors[(col - 1) % chart_colors.length]; + } } - } - document.addEventListener("DOMContentLoaded", () => { - //fade in loader - const loader = document.getElementById("div_spsa_preload"); - loader.style.display = ""; - loader.classList.add("fade"); - setTimeout(() => { - loader.classList.add("show"); - }, 150); + document.addEventListener("DOMContentLoaded", ()=>{ + //fade in loader + const loader = document.getElementById("div_spsa_preload"); + loader.style.display = ""; + loader.classList.add("fade"); + setTimeout(()=>{ + loader.classList.add("show"); + } + , 150); - //load google library - google.charts.load("current", { - packages: ["corechart"], - callback: function () { - const spsa_params = spsa_data.params; - const spsa_history = spsa_data.param_history; - const spsa_iter_ratio = Math.min( - spsa_data.iter / spsa_data.num_iter, - 1 - ); + //load google library + google.charts.load("current", { + packages: ["corechart"], + callback: function() { + const spsa_params = spsa_data.params; + const spsa_history = spsa_data.param_history; + const spsa_iter_ratio = Math.min(spsa_data.iter / spsa_data.num_iter, 1); - if (!spsa_history || spsa_history.length < 2) { - document.getElementById("div_spsa_preload").style.display = "none"; - const alertElement = document.createElement('div'); - alertElement.className = "alert alert-warning"; - alertElement.role = "alert"; - alertElement.textContent = "Not enough data to generate plot."; - const historyPlot = document.getElementById("div_spsa_history_plot"); - historyPlot.replaceChildren(); - historyPlot.append(alertElement); - return; - } + if (!spsa_history || spsa_history.length < 2) { + document.getElementById("div_spsa_preload").style.display = "none"; + const alertElement = document.createElement('div'); + alertElement.className = "alert alert-warning"; + alertElement.role = "alert"; + alertElement.textContent = "Not enough data to generate plot."; + const historyPlot = document.getElementById("div_spsa_history_plot"); + historyPlot.replaceChildren(); + historyPlot.append(alertElement); + return; + } - for (let i = 0; i < smoothing_max; i++) data_cache.push(false); + for (let i = 0; i < smoothing_max; i++) + data_cache.push(false); - let googleformat = []; - for (let i = 0; i < spsa_history.length; i++) { - let d = [(i / (spsa_history.length - 1)) * spsa_iter_ratio]; - for (let j = 0; j < spsa_params.length; j++) { - d.push(spsa_history[i][j].theta); - } - googleformat.push(d); - } + let googleformat = []; + for (let i = 0; i < spsa_history.length; i++) { + let d = [(i / (spsa_history.length - 1)) * spsa_iter_ratio]; + for (let j = 0; j < spsa_params.length; j++) { + d.push(spsa_history[i][j].theta); + } + googleformat.push(d); + } - chart_data = new google.visualization.DataTable(); + chart_data = new google.visualization.DataTable(); - chart_data.addColumn("number", "Iteration"); - for (let i = 0; i < spsa_params.length; i++) { - chart_data.addColumn("number", spsa_params[i].name); - } - chart_data.addRows(googleformat); + chart_data.addColumn("number", "Iteration"); + for (let i = 0; i < spsa_params.length; i++) { + chart_data.addColumn("number", spsa_params[i].name); + } + chart_data.addRows(googleformat); - data_cache[0] = chart_data; - chart_object = new google.visualization.LineChart( - document.getElementById("div_spsa_history_plot") - ); - chart_object.draw(chart_data, chart_options); - document.getElementById("chart_toolbar").style.display = ""; + data_cache[0] = chart_data; + chart_object = new google.visualization.LineChart(document.getElementById("div_spsa_history_plot")); + chart_object.draw(chart_data, chart_options); + document.getElementById("chart_toolbar").style.display = ""; - for (let i = 0; i < chart_data.getNumberOfColumns(); i++) { - columns.push(i); - } + for (let i = 0; i < chart_data.getNumberOfColumns(); i++) { + columns.push(i); + } - for (let j = 0; j < spsa_params.length; j++) { - const dropdownItem = document.createElement("li"); - const anchorItem = document.createElement("a"); - anchorItem.className = "dropdown-item"; - anchorItem.href = "javascript:"; - anchorItem.param_id = j + 1; - anchorItem.append(spsa_params[j].name); - dropdownItem.append(anchorItem); - document.getElementById("dropdown_individual").append(dropdownItem); - } + for (let j = 0; j < spsa_params.length; j++) { + const dropdownItem = document.createElement("li"); + const anchorItem = document.createElement("a"); + anchorItem.className = "dropdown-item"; + anchorItem.href = "javascript:"; + anchorItem.param_id = j + 1; + anchorItem.append(spsa_params[j].name); + dropdownItem.append(anchorItem); + document.getElementById("dropdown_individual").append(dropdownItem); + } - document - .getElementById("dropdown_individual") - .addEventListener("click", (e) => { - if (!e.target.matches("a")) return; - const { target } = e; - const param_id = target.param_id; - for (let i = 1; i < chart_data.getNumberOfColumns(); i++) { - update_column_visibility(i, i == param_id); - } + document.getElementById("dropdown_individual").addEventListener("click", (e)=>{ + if (!e.target.matches("a")) + return; + const {target} = e; + const param_id = target.param_id; + for (let i = 1; i < chart_data.getNumberOfColumns(); i++) { + update_column_visibility(i, i == param_id); + } - viewAll = false; - redraw(false); - }); + viewAll = false; + redraw(false); + } + ); - //show/hide functionality - google.visualization.events.addListener( - chart_object, - "select", - function (e) { - let sel = chart_object.getSelection(); - if (sel.length > 0 && sel[0].row == null) { - const col = sel[0].column; - update_column_visibility(col, columns[col] != col); - redraw(false); - } - viewAll = false; - } - ); + //show/hide functionality + google.visualization.events.addListener(chart_object, "select", function(e) { + let sel = chart_object.getSelection(); + if (sel.length > 0 && sel[0].row == null) { + const col = sel[0].column; + update_column_visibility(col, columns[col] != col); + redraw(false); + } + viewAll = false; + }); - document.getElementById("div_spsa_preload").style.display = "none"; + document.getElementById("div_spsa_preload").style.display = "none"; - document - .getElementById("btn_smooth_plus") - .addEventListener("click", () => { - if (smoothing_factor < smoothing_max) { - smooth_data(++smoothing_factor); - } - }); + document.getElementById("btn_smooth_plus").addEventListener("click", ()=>{ + if (smoothing_factor < smoothing_max) { + smooth_data(++smoothing_factor); + } + } + ); - document - .getElementById("btn_smooth_minus") - .addEventListener("click", () => { - if (smoothing_factor > 0) { - smooth_data(--smoothing_factor); - } - }); + document.getElementById("btn_smooth_minus").addEventListener("click", ()=>{ + if (smoothing_factor > 0) { + smooth_data(--smoothing_factor); + } + } + ); - document - .getElementById("btn_view_all") - .addEventListener("click", () => { - if (viewAll) return; - viewAll = true; - for (let i = 0; i < chart_data.getNumberOfColumns(); i++) { - update_column_visibility(i, true); - } + document.getElementById("btn_view_all").addEventListener("click", ()=>{ + if (viewAll) + return; + viewAll = true; + for (let i = 0; i < chart_data.getNumberOfColumns(); i++) { + update_column_visibility(i, true); + } - redraw(false); - }); - }, - }); - }); -})(); + redraw(false); + } + ); + }, + }); + } + ); +} +)(); diff --git a/server/fishtest/static/js/spsa_new.js b/server/fishtest/static/js/spsa_new.js index f4d8108df..fe7b8af83 100644 --- a/server/fishtest/static/js/spsa_new.js +++ b/server/fishtest/static/js/spsa_new.js @@ -6,90 +6,61 @@ chi2 distribution. */ function chi2_95_approximation(df) { - /* Wilson and Hilferty approximation */ - const z95 = 1.6448536269514722; - const t = 2 / (9 * df); - return df * Math.pow(z95 * Math.pow(t, 0.5) + 1 - t, 3); + /* Wilson and Hilferty approximation */ + const z95 = 1.6448536269514722; + const t = 2 / (9 * df); + return df * Math.pow(z95 * Math.pow(t, 0.5) + 1 - t, 3); } function chi2_95(df) { - /* Table for df=1,..,99 */ - const chi2_95_ = [ - 3.8414588206941236, 5.9914645471079799, 7.8147279032511765, - 9.487729036781154, 11.070497693516351, 12.591587243743977, - 14.067140449340167, 15.507313055865453, 16.918977604620448, - 18.307038053275143, 19.675137572682491, 21.026069817483066, - 22.362032494826941, 23.68479130484058, 24.99579013972863, - 26.296227604864235, 27.587111638275324, 28.869299430392626, - 30.143527205646159, 31.410432844230929, 32.670573340917294, - 33.9244384714438, 35.17246162690806, 36.415028501807299, 37.652484133482766, - 38.885138659830055, 40.113272069413611, 41.337138151427411, - 42.556967804292668, 43.772971825742182, 44.985343280365129, - 46.194259520278457, 47.399883919080921, 48.602367367294178, - 49.801849568201824, 50.99846016571064, 52.192319730102874, - 53.383540622969278, 54.572227758941736, 55.758479278887037, - 56.942387146824075, 58.124037680867971, 59.303512026899838, - 60.480886582336431, 61.656233376279538, 62.829620411408186, - 64.001111972218013, 65.170768903569808, 66.338648862968768, - 67.5048065495412, 68.669293912285838, 69.832160339848173, - 70.993452833782186, 72.153216167023089, 73.311493029083195, - 74.46832415930939, 75.623748469376167, 76.777803156061395, - 77.930523805230379, 79.081944487848745, 80.232097848762848, - 81.381015188899141, 82.528726541471897, 83.675260742721008, - 84.820645497656727, 85.964907441231006, 87.108072195321995, - 88.250164421874018, 89.391207872507835, 90.531225434880668, - 91.670239176054821, 92.808270383107839, 93.945339601192217, - 95.081466669243298, 96.216670753503891, 97.350970379033186, - 98.484383459340307, 99.616927324283921, 100.74861874635026, - 101.87947396543589, 103.00950871222616, 104.13873823027392, - 105.26717729686055, 106.39484024272251, 107.52174097071949, - 108.64789297350764, 109.77330935028799, 110.89800282268439, - 112.02198574980785, 113.1452701425555, 114.26786767719352, - 115.38978970826668, 116.51104728087367, 117.63165114234559, - 118.75161175336743, 119.87093929856709, 120.98964369660951, - 122.10773460981952, 123.22522145336157, - ]; - return df <= 99 ? chi2_95_[df - 1] : chi2_95_approximation(df); + /* Table for df=1,..,99 */ + const chi2_95_ = [3.8414588206941236, 5.9914645471079799, 7.8147279032511765, 9.487729036781154, 11.070497693516351, 12.591587243743977, 14.067140449340167, 15.507313055865453, 16.918977604620448, 18.307038053275143, 19.675137572682491, 21.026069817483066, 22.362032494826941, 23.68479130484058, 24.99579013972863, 26.296227604864235, 27.587111638275324, 28.869299430392626, 30.143527205646159, 31.410432844230929, 32.670573340917294, 33.9244384714438, 35.17246162690806, 36.415028501807299, 37.652484133482766, 38.885138659830055, 40.113272069413611, 41.337138151427411, 42.556967804292668, 43.772971825742182, 44.985343280365129, 46.194259520278457, 47.399883919080921, 48.602367367294178, 49.801849568201824, 50.99846016571064, 52.192319730102874, 53.383540622969278, 54.572227758941736, 55.758479278887037, 56.942387146824075, 58.124037680867971, 59.303512026899838, 60.480886582336431, 61.656233376279538, 62.829620411408186, 64.001111972218013, 65.170768903569808, 66.338648862968768, 67.5048065495412, 68.669293912285838, 69.832160339848173, 70.993452833782186, 72.153216167023089, 73.311493029083195, 74.46832415930939, 75.623748469376167, 76.777803156061395, 77.930523805230379, 79.081944487848745, 80.232097848762848, 81.381015188899141, 82.528726541471897, 83.675260742721008, 84.820645497656727, 85.964907441231006, 87.108072195321995, 88.250164421874018, 89.391207872507835, 90.531225434880668, 91.670239176054821, 92.808270383107839, 93.945339601192217, 95.081466669243298, 96.216670753503891, 97.350970379033186, 98.484383459340307, 99.616927324283921, 100.74861874635026, 101.87947396543589, 103.00950871222616, 104.13873823027392, 105.26717729686055, 106.39484024272251, 107.52174097071949, 108.64789297350764, 109.77330935028799, 110.89800282268439, 112.02198574980785, 113.1452701425555, 114.26786767719352, 115.38978970826668, 116.51104728087367, 117.63165114234559, 118.75161175336743, 119.87093929856709, 120.98964369660951, 122.10773460981952, 123.22522145336157, ]; + return df <= 99 ? chi2_95_[df - 1] : chi2_95_approximation(df); } const spsa_setup_default = { - num_params: 1, - draw_ratio: 0.739 /* "virtual" draw_ratio(STC) */, - precision: 0.5, - c_ratio: 1 / 6, - lambda_ratio: 3, - params: [ - { name: "dummy", start: 50, min: 0, max: 100, elo: 2, c: null, r: null }, - ], - num_games: null, + num_params: 1, + draw_ratio: 0.739 /* "virtual" draw_ratio(STC) */ + , + precision: 0.5, + c_ratio: 1 / 6, + lambda_ratio: 3, + params: [{ + name: "dummy", + start: 50, + min: 0, + max: 100, + elo: 2, + c: null, + r: null + }, ], + num_games: null, }; function deepcopy(o) { - return JSON.parse(JSON.stringify(o)); + return JSON.parse(JSON.stringify(o)); } function spsa_compute(spsa_setup) { - const C = 347.43558552260146; - let s = deepcopy(spsa_setup); - const chi2 = chi2_95(s.num_params); - const r = s.precision / ((C * chi2 * (1 - s.draw_ratio)) / 8); - let lambda = new Array(s.num_params); - for (let i = 0; i < s.num_params; i++) { - s.params[i].c = s.c_ratio * (s.params[i].max - s.params[i].min); - s.params[i].r = r; - const H_diag = - (-2 * s.params[i].elo) / - Math.pow((s.params[i].max - s.params[i].min) / 2, 2); - lambda[i] = -C / (2 * r * Math.pow(s.params[i].c, 2) * H_diag); - } - s.num_games = -1; - for (let i = 0; i < s.num_params; i++) { - const ng = Math.round(s.lambda_ratio * lambda[i]); - if (ng > s.num_games) { - s.num_games = ng; + const C = 347.43558552260146; + let s = deepcopy(spsa_setup); + const chi2 = chi2_95(s.num_params); + const r = s.precision / ((C * chi2 * (1 - s.draw_ratio)) / 8); + let lambda = new Array(s.num_params); + for (let i = 0; i < s.num_params; i++) { + s.params[i].c = s.c_ratio * (s.params[i].max - s.params[i].min); + s.params[i].r = r; + const H_diag = (-2 * s.params[i].elo) / Math.pow((s.params[i].max - s.params[i].min) / 2, 2); + lambda[i] = -C / (2 * r * Math.pow(s.params[i].c, 2) * H_diag); } - } - return s; + s.num_games = -1; + for (let i = 0; i < s.num_params; i++) { + const ng = Math.round(s.lambda_ratio * lambda[i]); + if (ng > s.num_games) { + s.num_games = ng; + } + } + return s; } /* @@ -99,70 +70,69 @@ a few data points valid for the book "UHO_XXL_+0.90_+1.19.epd". */ function tc_to_seconds(tc) { - /* + /* Convert cutechess-cli like tc time[/moves][+inc] to seconds/move. */ - let inc = 0; - let moves = 68; /* Fishtest average LTC game duration. */ - let time; - let chunks = tc.split("+"); - if (chunks.length > 2) { - return null; - } - if (chunks.length == 2) { - inc = parseFloat(chunks[1]); - if (!isFinite(Number(chunks[1])) || !isFinite(inc) || inc < 0) { - return null; + let inc = 0; + let moves = 68; + /* Fishtest average LTC game duration. */ + let time; + let chunks = tc.split("+"); + if (chunks.length > 2) { + return null; + } + if (chunks.length == 2) { + inc = parseFloat(chunks[1]); + if (!isFinite(Number(chunks[1])) || !isFinite(inc) || inc < 0) { + return null; + } + } + chunks = chunks[0].split("/"); + if (chunks.length > 2) { + return null; } - } - chunks = chunks[0].split("/"); - if (chunks.length > 2) { - return null; - } - if (chunks.length == 2) { - moves = parseInt(chunks[1]); - if (!isFinite(Number(chunks[1])) || !isFinite(moves) || moves <= 0) { - return null; + if (chunks.length == 2) { + moves = parseInt(chunks[1]); + if (!isFinite(Number(chunks[1])) || !isFinite(moves) || moves <= 0) { + return null; + } } - } - chunks = chunks[0].split(":"); - if (chunks.length > 2) { - return null; - } - const chunk0 = parseFloat(chunks[0]); - if (!isFinite(Number(chunks[0])) || !isFinite(chunk0) || chunk0 < 0) { - return null; - } - if (chunks.length == 1) { - time = chunk0; - } else { - const chunk1 = parseFloat(chunks[1]); - if (!isFinite(Number(chunks[1])) || !isFinite(chunk1) || chunk1 < 0) { - return null; + chunks = chunks[0].split(":"); + if (chunks.length > 2) { + return null; } - time = 60 * chunk0 + chunk1; - } - const tc_seconds = time / moves + inc; - /* Adhoc for our application: do not allow zero time control */ - return tc_seconds > 0 ? tc_seconds : null; + const chunk0 = parseFloat(chunks[0]); + if (!isFinite(Number(chunks[0])) || !isFinite(chunk0) || chunk0 < 0) { + return null; + } + if (chunks.length == 1) { + time = chunk0; + } else { + const chunk1 = parseFloat(chunks[1]); + if (!isFinite(Number(chunks[1])) || !isFinite(chunk1) || chunk1 < 0) { + return null; + } + time = 60 * chunk0 + chunk1; + } + const tc_seconds = time / moves + inc; + /* Adhoc for our application: do not allow zero time control */ + return tc_seconds > 0 ? tc_seconds : null; } function logistic(x) { - return 1 / (1 + Math.exp(-x)); + return 1 / (1 + Math.exp(-x)); } function draw_ratio(tc) { - /* + /* Formula approximately valid for the book "UHO_XXL_+0.90_+1.19.epd". The "virtual" draw ratio of an unbalanced book is defined as 1 - 4 * ("pentanomial variance/game") */ - const slope = 0.22; - const intercept = 1.35; - const tc_seconds = tc_to_seconds(tc); - return tc_seconds !== null - ? logistic(slope * Math.log(tc_seconds) + intercept) - : null; + const slope = 0.22; + const intercept = 1.35; + const tc_seconds = tc_to_seconds(tc); + return tc_seconds !== null ? logistic(slope * Math.log(tc_seconds) + intercept) : null; } /* @@ -171,82 +141,70 @@ objects and back. */ function fishtest_to_spsa(fs) { - let s = deepcopy(spsa_setup_default); - const lines = fs.split("\n"); - let j = 0; - s.params = []; - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line == "") { - continue; - } - - s.params.push({}); - - s.params[j].elo = 2; - - const chunks = line.split(","); - if (chunks.length != 6) { - return null; - } - - const name = chunks[0]; - - const start = parseFloat(chunks[1]); - if (!isFinite(start)) { - return null; - } - - const min = parseFloat(chunks[2]); - if (!isFinite(min)) { - return null; + let s = deepcopy(spsa_setup_default); + const lines = fs.split("\n"); + let j = 0; + s.params = []; + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line == "") { + continue; + } + + s.params.push({}); + + s.params[j].elo = 2; + + const chunks = line.split(","); + if (chunks.length != 6) { + return null; + } + + const name = chunks[0]; + + const start = parseFloat(chunks[1]); + if (!isFinite(start)) { + return null; + } + + const min = parseFloat(chunks[2]); + if (!isFinite(min)) { + return null; + } + + const max = parseFloat(chunks[3]); + if (!isFinite(max) || max <= min) { + return null; + } + + const c = parseFloat(chunks[4]); + if (!isFinite(c) || c <= 0) { + return null; + } + + const r = parseFloat(chunks[5]); + if (!isFinite(r) || r <= 0) { + return null; + } + + s.params[j].name = name; + s.params[j].start = start; + s.params[j].min = min; + s.params[j].max = max; + s.params[j].c = c; + s.params[j].r = r; + + j++; } - - const max = parseFloat(chunks[3]); - if (!isFinite(max) || max <= min) { - return null; - } - - const c = parseFloat(chunks[4]); - if (!isFinite(c) || c <= 0) { - return null; - } - - const r = parseFloat(chunks[5]); - if (!isFinite(r) || r <= 0) { - return null; - } - - s.params[j].name = name; - s.params[j].start = start; - s.params[j].min = min; - s.params[j].max = max; - s.params[j].c = c; - s.params[j].r = r; - - j++; - } - s.num_params = s.params.length; - return s.num_params > 0 ? s : null; + s.num_params = s.params.length; + return s.num_params > 0 ? s : null; } function spsa_to_fishtest(ss) { - let ret = ""; - for (let i = 0; i < ss.params.length; i++) { - const p = ss.params; - ret += - p[i].name + - "," + - p[i].start + - "," + - p[i].min + - "," + - p[i].max + - "," + - p[i].c.toFixed(2) + - "," + - p[i].r.toFixed(5) + - "\n"; - } - return ret; + let ret = ""; + for (let i = 0; i < ss.params.length; i++) { + const p = ss.params; + ret += p[i].name + "," + p[i].start + "," + p[i].min + "," + p[i].max + "," + p[i].c.toFixed(2) + "," + p[i].r.toFixed(5) + "\n"; + } + return ret; } diff --git a/server/fishtest/static/js/toggle_password.js b/server/fishtest/static/js/toggle_password.js index 674e93d8c..8bc4d2372 100644 --- a/server/fishtest/static/js/toggle_password.js +++ b/server/fishtest/static/js/toggle_password.js @@ -1,14 +1,17 @@ -(() => { - "use strict"; +(()=>{ + "use strict"; - const togglePasswordVisibility = document.querySelectorAll('.toggle-password-visibility'); - togglePasswordVisibility.forEach(toggle => { - toggle.addEventListener('click', event => { - const input = event.target.parentNode.querySelector('input'); - const icon = event.target.querySelector('i'); - input.type = input.type === 'password' ? 'text' : 'password'; - icon.classList.toggle('fa-eye'); - icon.classList.toggle('fa-eye-slash'); - }); - }); -})() + const togglePasswordVisibility = document.querySelectorAll('.toggle-password-visibility'); + togglePasswordVisibility.forEach(toggle=>{ + toggle.addEventListener('click', event=>{ + const input = event.target.parentNode.querySelector('input'); + const icon = event.target.querySelector('i'); + input.type = input.type === 'password' ? 'text' : 'password'; + icon.classList.toggle('fa-eye'); + icon.classList.toggle('fa-eye-slash'); + } + ); + } + ); +} +)()