diff --git a/public/elotracker/chart-integration.js b/public/elotracker/chart-integration.js index 475961c..f96682a 100644 --- a/public/elotracker/chart-integration.js +++ b/public/elotracker/chart-integration.js @@ -1,9 +1,99 @@ -window.onload = async function(){ - let all_dates = []; // Alle Daten für die für min. einen Spieler ein Rang gespeichert ist - let all_rank_yDatasets = []; // Datasets, also die einzelnen Linien im Graphen, beinhaltet player_points - let all_progress_yDatasets = []; // Datasets, also die einzelnen Linien im Graphen, beinhaltet die Differenz der aktuellen player_points und der start player_points - let player_points = {}; // puuid zeigt auf eine Liste von Punkten, welche die y-Koordinaten im Graphen darstellen - let player_pointprogress = {}; // puuid zeigt auf eine Liste von Punkten, welche die y-Koordinaten im Graphen darstellen +let combined_charts = []; +let player_charts = []; + +const x_scale = { + display: true, + type: "timeseries", + time: { + parser: "dd.MM.yy HH:mm", + tooltipFormat: "dd.MM.yy HH:mm", + unit: "day", + unitStepSize: 1, + displayFormats: { + "day": "dd.MM.yy", + } + }, + ticks: { + callback: (value, index, ticks) => { + let tickdate = new Date(value); + if (index === 0) return `${tickdate.getDate()}.${month_names[tickdate.getMonth()]}`; + let prev_tickdate = new Date(ticks[index-1].value); + if (prev_tickdate.getDate() === tickdate.getDate()) { + return null; + } + return `${tickdate.getDate()}.${month_names[tickdate.getMonth()]}`; + } + } +} +const zoom_plugin = { + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: true, + }, + mode: "x", + }, + pan: { + enabled: true, + mode: "x", + }, +} +const trigger_legend_hover = function (e, legendItem, legend) { + if (legendItem.hidden) { + return; + } + legend.chart.data.datasets.forEach((dataset, i) => { + dataset.backgroundColor = (legendItem.datasetIndex === i || dataset.backgroundColor.length === 9) ? dataset.backgroundColor : dataset.backgroundColor + '3D'; + dataset.borderColor = (legendItem.datasetIndex === i || dataset.borderColor.length === 9) ? dataset.borderColor : dataset.borderColor + '3D'; + dataset.borderWidth = legendItem.datasetIndex === i ? 4 : 3; + dataset.pointRadius = 2; + }); + legend.chart.update(); +} +const trigger_legend_leave = function (e, legendItem, legend) { + legend.chart.data.datasets.forEach((dataset, i) => { + dataset.backgroundColor = dataset.backgroundColor.length === 9 ? dataset.backgroundColor.slice(0, -2) : dataset.backgroundColor; + dataset.borderColor = dataset.borderColor.length === 9 ? dataset.borderColor.slice(0, -2) : dataset.borderColor; + dataset.borderWidth = 3; + }); + legend.chart.update(); +} +const legend_plugin = { + position: "bottom", + labels: { + padding: 10, + useBorderRadius: true, + borderRadius: 2, + boxWidth: 20, + }, + onHover: trigger_legend_hover, + onLeave: trigger_legend_leave, + onClick: function (e, legendItem, legend) { + if (legend.chart.isDatasetVisible(legendItem.datasetIndex)) { + trigger_legend_leave(e, legendItem, legend); + legend.chart.hide(legendItem.datasetIndex); + legendItem.hidden = true; + } else { + legend.chart.show(legendItem.datasetIndex); + legendItem.hidden = false; + trigger_legend_hover(e, legendItem, legend); + } + } +} + +window.onload = create_charts; + +async function create_charts() { + combined_charts.forEach(chart => chart.destroy()); + combined_charts = []; + player_charts.forEach(chart => chart.destroy()); + player_charts = []; + + let player_rank_values = {}; + let player_progress_values = {}; + let player_entries = {}; // puuid zeigt auf player entries let player_entries_byName = {}; // playername zeigt auf entries (damit im kombinierten Graphen die Tooltips korrekt gerendert werden können) let player_accounts = {}; // puuid zeigt auf player account @@ -15,39 +105,28 @@ window.onload = async function(){ .then(result => { player_entries = result["entries"]; player_accounts = result["accounts"]; - for (const puuid in player_entries) { - // Alle Daten für die Spieler Einträge haben sollen in all_dates - for (const entry_date in player_entries[puuid]) { - all_dates.push(entry_date) - } - // Hier schonmal puuids/namen auf leere Arrays setzen, damit später einfach .push verwendet werden kann - player_points[puuid] = []; - player_pointprogress[puuid] = []; - player_entries_byName[`${player_accounts[puuid]["gameName"]}#${player_accounts[puuid]["tagLine"]}`] = []; - } - // Daten sortieren und Duplicates entfernen - all_dates.sort(); - all_dates = [...new Set(all_dates)]; + + let all_rank_yDatasets = []; // Datasets, also die einzelnen Linien im Graphen, beinhaltet player_rank_values + let all_progress_yDatasets = []; // Datasets, also die einzelnen Linien im Graphen, beinhaltet player_progress_values let color_counter = 0; for (const puuid in player_entries) { + player_rank_values[puuid] = []; + player_progress_values[puuid] = []; + player_entries_byName[`${player_accounts[puuid]["gameName"]}#${player_accounts[puuid]["tagLine"]}`] = []; + let player_start_points = -1; - for (const date_string of all_dates) { - // Für alle Player Entries Punktzahl berechnen und in player_points eintragen + for (const timestamp in player_entries[puuid]) { + // Für alle Player Entries Punktzahl und Punktedifferenz berechnen // Bei der Gelegenheit auch Entries in player_entries_byName eintragen - if (date_string in player_entries[puuid]) { - const current_points = rank_to_points(player_entries[puuid][date_string]["tier"],player_entries[puuid][date_string]["rank"],player_entries[puuid][date_string]["points"]); - player_points[puuid].push(current_points); - player_entries_byName[`${player_accounts[puuid]["gameName"]}#${player_accounts[puuid]["tagLine"]}`][date_string] = player_entries[puuid][date_string]; - if (player_start_points === -1) { - player_start_points = current_points; - player_pointprogress[puuid].push(0); - } else { - player_pointprogress[puuid].push(current_points - player_start_points); - } + const current_points = rank_to_points(player_entries[puuid][timestamp]["tier"], player_entries[puuid][timestamp]["rank"], player_entries[puuid][timestamp]["points"]); + player_rank_values[puuid].push({x: parseInt(timestamp) * 1000, y: current_points}); + player_entries_byName[`${player_accounts[puuid]["gameName"]}#${player_accounts[puuid]["tagLine"]}`][timestamp] = player_entries[puuid][timestamp]; + if (player_start_points === -1) { + player_start_points = current_points; + player_progress_values[puuid].push({x: parseInt(timestamp) * 1000, y: 0}); } else { - player_points[puuid].push(null); - player_pointprogress[puuid].push(null); + player_progress_values[puuid].push({x: parseInt(timestamp) * 1000, y: current_points - player_start_points}); } } // Linie für den Spieler zu Datasets des Graphen hinzufügen @@ -56,24 +135,25 @@ window.onload = async function(){ fill: false, borderColor: getColor(color_counter), backgroundColor: getColor(color_counter), - data: player_points[puuid], + data: player_rank_values[puuid], spanGaps: true, + pointHitRadius: 16, }) all_progress_yDatasets.push({ label: `${player_accounts[puuid]["gameName"]}#${player_accounts[puuid]["tagLine"]}`, fill: false, borderColor: getColor(color_counter), backgroundColor: getColor(color_counter), - data: player_pointprogress[puuid], + data: player_progress_values[puuid], spanGaps: true, + pointHitRadius: 16, }) color_counter++; } // Graphen erstellen - new Chart(`progress-chart-combined`, { + combined_charts.push(new Chart(`progress-chart-combined`, { type: "line", data: { - labels: all_dates, datasets: all_rank_yDatasets, }, options: { @@ -81,15 +161,18 @@ window.onload = async function(){ tooltip: { callbacks: { label: function(context) { - return format_rank(player_entries_byName[context.dataset.label][context.label]["tier"],player_entries_byName[context.dataset.label][context.label]["rank"],player_entries_byName[context.dataset.label][context.label]["points"]) + return format_rank(player_entries_byName[context.dataset.label][context.parsed.x/1000]["tier"],player_entries_byName[context.dataset.label][context.parsed.x/1000]["rank"],player_entries_byName[context.dataset.label][context.parsed.x/1000]["points"]) }, beforeTitle: function (context) { return context[0].dataset.label; }, } - } + }, + zoom: zoom_plugin, + legend: legend_plugin, }, scales: { + x: x_scale, y: { display: true, stepSize: 100, @@ -103,11 +186,10 @@ window.onload = async function(){ responsive: true, maintainAspectRatio: false, } - }); - new Chart(`progress-chart-combined-progress`, { + })); + combined_charts.push(new Chart(`progress-chart-combined-progress`, { type: "line", data: { - labels: all_dates, datasets: all_progress_yDatasets, }, options: { @@ -115,15 +197,18 @@ window.onload = async function(){ tooltip: { callbacks: { label: function(context) { - return format_rank(player_entries_byName[context.dataset.label][context.label]["tier"],player_entries_byName[context.dataset.label][context.label]["rank"],player_entries_byName[context.dataset.label][context.label]["points"]) + return format_rank(player_entries_byName[context.dataset.label][context.parsed.x/1000]["tier"],player_entries_byName[context.dataset.label][context.parsed.x/1000]["rank"],player_entries_byName[context.dataset.label][context.parsed.x/1000]["points"]) }, beforeTitle: function (context) { return context[0].dataset.label; }, } - } + }, + zoom: zoom_plugin, + legend: legend_plugin, }, scales: { + x: x_scale, y: { display: true, }, @@ -131,7 +216,7 @@ window.onload = async function(){ responsive: true, maintainAspectRatio: false, } - }); + })); }) .catch(e => console.error(e)) @@ -140,23 +225,21 @@ window.onload = async function(){ let puuid = chart.id.split("-"); puuid.splice(0,2); puuid = puuid.join("-"); - let xValues = []; - let yValues = []; + let values = []; for (const entriesKey in player_entries[puuid]) { let points = rank_to_points(player_entries[puuid][entriesKey]["tier"], player_entries[puuid][entriesKey]["rank"], player_entries[puuid][entriesKey]["points"]); - xValues.push(entriesKey); - yValues.push(points); + values.push({x: parseInt(entriesKey)*1000, y: points}); } - new Chart(`progress-chart-${puuid}`, { + player_charts.push(new Chart(`progress-chart-${puuid}`, { type: "line", data: { - labels: xValues, datasets: [{ label: `${player_accounts[puuid]["gameName"]}#${player_accounts[puuid]["tagLine"]}`, fill: false, borderColor: "rgba(150,150,175)", backgroundColor: "rgba(150,150,175)", - data: yValues + data: values, + pointHitRadius: 16, }] }, options: { @@ -165,15 +248,17 @@ window.onload = async function(){ tooltip: { callbacks: { label: function(context) { - return format_rank(player_entries[puuid][context.label]["tier"],player_entries[puuid][context.label]["rank"],player_entries[puuid][context.label]["points"]) + return format_rank(player_entries[puuid][context.parsed.x/1000]["tier"],player_entries[puuid][context.parsed.x/1000]["rank"],player_entries[puuid][context.parsed.x/1000]["points"]) }, beforeTitle: function (context) { return context[0].dataset.label; }, } - } + }, + zoom: zoom_plugin, }, scales: { + x: x_scale, y: { display: true, stepSize: 100, @@ -187,10 +272,9 @@ window.onload = async function(){ responsive: true, maintainAspectRatio: false, } - }); + })); } - -}; +} function rank_to_points(tier,rank,lp) { const apex_tiers = (tier === "MASTER" || tier === "GRANDMASTER" || tier === "CHALLENGER"); @@ -267,12 +351,80 @@ function toggle_combined_chart() { chart.classList.add("closed"); } } -function toggle_leaderboard_chart() { +function toggle_leaderboard_chart(event) { if (this.classList.contains("closed")) { this.classList.remove("closed"); } else { + if (event.target.nodeName === "CANVAS") return; this.classList.add("closed"); } } document.querySelectorAll("button.open-general-graph").forEach(element => element.addEventListener("click", toggle_combined_chart)); -document.querySelectorAll("button.leaderboard-element").forEach(element => element.addEventListener("click", toggle_leaderboard_chart)); \ No newline at end of file +document.querySelectorAll("button.leaderboard-element").forEach(element => element.addEventListener("mousedown", toggle_leaderboard_chart)); + +async function update_leaderboard_entries() { + this.disabled = true; + this.classList.add("button-updating") + let eventSource = new EventSource("./update.php"); + eventSource.addEventListener("progress", e => { + this.style.setProperty("--button-loading-bar-width", `${e.data*100}%`); + }) + eventSource.addEventListener("forbidden", e => { + const currenttime = new Date(); + const updatetime = new Date(e.data); + let timediff = new Date(currenttime - updatetime); + let resttime = new Date(600000 - (currenttime - updatetime)); + window.alert(`Das letzte Update wurde vor ${format_time_minsec(timediff)} durchgeführt. Versuche es in ${format_time_minsec(resttime)} noch einmal`); + reset_button(this); + eventSource.close(); + }) + eventSource.onerror = e => { + window.alert(`Beim Update ist ein Fehler aufgetreten. Versuche es später noch einmal`) + reset_button(this); + eventSource.close(); + } + eventSource.addEventListener("done", e => { + reset_button(this); + eventSource.close(); + update_leaderboard_elements(); + }) + function reset_button(button) { + button.disabled = false; + button.style.setProperty("--button-loading-bar-width", `0`); + button.classList.remove("button-updating"); + } +} + +document.querySelector("button.update-leaderboard").addEventListener("click", update_leaderboard_entries); + +function format_time_minsec(date) { + let format, trenner = "", min = "", nullausgleich = ""; + if (date.getMinutes() === 0) { + format = " Sekunden"; + } else { + min = date.getMinutes(); + format = " Minuten"; + trenner = ":"; + if (date.getSeconds() < 10) { + nullausgleich = "0"; + } + } + return min + trenner + nullausgleich + date.getSeconds() + format; +} +const month_names = ["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]; + +async function update_leaderboard_elements() { + fetch(`./leaderboard_list.php`, { + method: "GET", + }) + .then(res => res.text()) + .then(leaderboard_list => { + // replace updated Leaderboard + document.querySelector(".leaderboard-list").outerHTML = leaderboard_list; + // reapply EventListeners + document.querySelectorAll("button.leaderboard-element").forEach(element => element.addEventListener("click", toggle_leaderboard_chart)); + // recreate charts + create_charts(); + }) + .catch(e => console.error(e)); +} \ No newline at end of file diff --git a/public/elotracker/index.php b/public/elotracker/index.php index b587c63..9eb9ac8 100644 --- a/public/elotracker/index.php +++ b/public/elotracker/index.php @@ -4,81 +4,25 @@ UEM Elo Tracker - + + + + -UEM LoL Elo-Challenge"; -echo "
"; -echo "
"; -echo "
- - -
"; -echo "
"; -echo "
"; -echo "
"; -$placement_counter = 1; -foreach ($tracker->getProgressions() as $puuid => $progress){ - $entries = $tracker->entries[$puuid]; - $account = $tracker->accounts[$puuid]; - $start_elo = reset($entries); - $start_tier = ($start_elo) ? $start_elo->tier : "UNRANKED"; - if ($start_tier == "MASTER" || $start_tier == "GRANDMASTER" || $start_tier == "CHALLENGER") $start_elo->rank = ""; - $current_elo = end($entries); - $current_tier = ($current_elo) ? $current_elo->tier : "UNRANKED"; - if ($current_tier == "MASTER" || $current_tier == "GRANDMASTER" || $current_tier == "CHALLENGER") $current_elo->rank = ""; - $progress = ($progress>=0) ? "+$progress" : $progress; - $tier_lowercase = strtolower($current_tier); - $start_tier_lowercase = strtolower($start_tier); - $tier_ucfirst = ucfirst($tier_lowercase); - $start_tier_ucfirst = ucfirst($start_tier_lowercase); - $gained_lp_sign = ($progress > 0) ? " plus-lp" : (($progress < 0) ? " minus-lp" : "no-lp"); - - if ($start_elo) { - echo ""; - $placement_counter++; -} -echo "
"; -echo "
"; - -?> - +

UEM LoL Elo-Challenge

+
+
+ +
+ + +
+
+
+ +
+ diff --git a/public/elotracker/leaderboard_list.php b/public/elotracker/leaderboard_list.php new file mode 100644 index 0000000..035a1bd --- /dev/null +++ b/public/elotracker/leaderboard_list.php @@ -0,0 +1,57 @@ +"; +$placement_counter = 1; +foreach ($tracker->getProgressions() as $puuid => $progress) { + $entries = $tracker->entries[$puuid]; + $account = $tracker->accounts[$puuid]; + $start_elo = reset($entries); + $start_tier = ($start_elo) ? $start_elo->tier : "UNRANKED"; + if ($start_tier == "MASTER" || $start_tier == "GRANDMASTER" || $start_tier == "CHALLENGER") $start_elo->rank = ""; + $current_elo = end($entries); + $current_tier = ($current_elo) ? $current_elo->tier : "UNRANKED"; + if ($current_tier == "MASTER" || $current_tier == "GRANDMASTER" || $current_tier == "CHALLENGER") $current_elo->rank = ""; + $progress = ($progress >= 0) ? "+$progress" : $progress; + $tier_lowercase = strtolower($current_tier); + $start_tier_lowercase = strtolower($start_tier); + $tier_ucfirst = ucfirst($tier_lowercase); + $start_tier_ucfirst = ucfirst($start_tier_lowercase); + $gained_lp_sign = ($progress > 0) ? " plus-lp" : (($progress < 0) ? " minus-lp" : "no-lp"); + + if ($start_elo) { + echo ""; + $placement_counter++; +} +echo ""; diff --git a/public/elotracker/styles.css b/public/elotracker/styles.css index 5480c7c..8517683 100644 --- a/public/elotracker/styles.css +++ b/public/elotracker/styles.css @@ -20,12 +20,17 @@ h1 { button { background: none; color: inherit; - border: none; - padding: 0; + border: solid 2px #68a8cc; + border-radius: 12px; + padding: 16px 24px; font: inherit; cursor: pointer; outline: inherit; text-align: unset; + transition: background-color 200ms ease-out; +} +button:hover { + background-color: #1f2931; } button[disabled] { cursor: initial; @@ -44,14 +49,34 @@ img.uem-logo { height: 120px; } -button.open-general-graph { - border: solid 2px #68a8cc; - border-radius: 12px; - padding: 16px 24px; - transition: background-color 200ms ease-out; +button.update-leaderboard { + display: flex; + flex-direction: row; + gap: 4px; + align-items: center; + margin-bottom: 8px; } -button.open-general-graph:hover { - background-color: #1f2931; +button.progress-button { + position: relative; + overflow: hidden; + --button-loading-bar-width: 0; +} +button.progress-button::before { + content: ''; + position: absolute; + width: var(--button-loading-bar-width); + height: 100%; + left: 0; + background-color: #294c87; + border-radius: 10px; + transition: width 400ms ease-out; + z-index: -1; +} +button.button-updating svg { + animation: ease_rotating 2s infinite; +} +@keyframes ease_rotating { + to { transform: rotate(-360deg)} } .leaderboard { @@ -76,11 +101,15 @@ button.open-general-graph:hover { border: solid 4px #6a818f; border-radius: 20px; padding: 16px 16px 16px 36px; - transition: background-color 200ms ease-out; + transition: background-color 200ms ease; } .leaderboard-element:hover { background-color: #1f2931; } +.leaderboard-element:has(canvas:hover) { + background-color: transparent; + cursor: initial; +} .leaderboard-element.place-1 { border: solid 4px var(--gold-1); } diff --git a/public/img/material-symbols/sync_24dp_FILL1_wght300_GRAD0_opsz24.svg b/public/img/material-symbols/sync_24dp_FILL1_wght300_GRAD0_opsz24.svg new file mode 100644 index 0000000..ba8078c --- /dev/null +++ b/public/img/material-symbols/sync_24dp_FILL1_wght300_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/index.php b/public/index.php index 3e3a781..2cf3f64 100644 --- a/public/index.php +++ b/public/index.php @@ -4,25 +4,6 @@ University eSports Mainz - Zum Elo Tracker diff --git a/public/util/mysql_connect.php b/public/util/mysql_connect.php index db546d1..3e2626a 100644 --- a/public/util/mysql_connect.php +++ b/public/util/mysql_connect.php @@ -22,6 +22,7 @@ class MySQLConnection { private function createConn(): void { $this->mysqli = new mysqli($this->host . ":" . $this->port, $this->user, $this->pass, $this->dbName); + $this->mysqli->set_charset("utf8"); } public function query($sql): mysqli_result|bool {