let combined_charts = []; let player_charts = []; const x_scale = { display: true, type: "linear", stepSize: 1000 * 60 * 60 * 24, ticks: { callback: (value, index, ticks) => { let tickdate = new Date(value); return `${tickdate.getDate()}.${month_names[tickdate.getMonth()]}`; } } } const zoom_plugin = { zoom: { wheel: { enabled: true, }, pinch: { enabled: true, }, mode: "x", onZoomComplete: function({chart}) { const show_point_cutoff = 1000 * 60 * 60 * 24 * 3 const diff = parseInt(chart.scales.x.max) - parseInt(chart.scales.x.min); const show_points = diff < show_point_cutoff; const hide_points = diff >= show_point_cutoff; let update_chart = false; chart.data.datasets.forEach(dataset => { const points_shown = dataset.pointRadius > 0 if (show_points && !points_shown) { dataset.pointRadius = 3; update_chart = true; } if (hide_points && points_shown) { dataset.pointRadius = 0; update_chart = true; } }) if (update_chart) chart.update(); } }, pan: { enabled: true, mode: "x", }, } let hovered = false; const trigger_legend_hover = function (legendItem, chart) { if (legendItem.hidden || hovered) { return; } hovered = true; 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 = dataset.pointRadius===0 ? 0 : 2; }); chart.update(); } const trigger_legend_leave = function (chart) { 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; }); chart.update(); hovered = false; } const trigger_legend_click = function (item) { combined_charts.forEach(c_chart => { if (c_chart.isDatasetVisible(item.datasetIndex)) { trigger_legend_leave(c_chart) c_chart.hide(item.datasetIndex); item.hidden = true; } else { c_chart.show(item.datasetIndex); item.hidden = false; trigger_legend_hover(item, c_chart) } }) } // break hover state when mouseout fails to trigger window.onmousemove = (e) => { if (hovered && (e.target.closest("ul") === null || !e.target.closest("ul").classList.contains("legend-list"))) { console.log("breaking hover") combined_charts.forEach(chart => { trigger_legend_leave(chart); }) } } const get_create_LegendList = (chart, id) => { let created = false; const legendContainer = document.getElementById(id); let listContainer = legendContainer.querySelector("ul"); if (!listContainer) { created = true; listContainer = document.createElement("ul"); listContainer.classList.add('legend-list'); legendContainer.appendChild(listContainer); } return {list: listContainer, created: created}; } const htmlLegendPlugin = { id: 'htmlLegend', afterUpdate(chart, args, options) { //console.log("update") const legendList = get_create_LegendList(chart, options.containerID); const ul = legendList.list; // Reuse the built-in legendItems generator const items = chart.options.plugins.legend.labels.generateLabels(chart); items.forEach(item => { if (legendList.created) { const li = document.createElement('li'); li.classList.add("legend-element"); li.onmouseover = () => {trigger_legend_hover(item, chart)}; li.onmouseout = () => {trigger_legend_leave(chart)}; li.onclick = () => {trigger_legend_click(item)}; // Color box const boxSpan = document.createElement('span'); boxSpan.style.background = item.fillStyle; boxSpan.style.borderColor = item.strokeStyle; boxSpan.style.borderWidth = item.lineWidth + 'px'; // Text const textContainer = document.createElement('p'); textContainer.style.color = item.fontColor; textContainer.style.textDecoration = item.hidden ? 'line-through' : ''; const text = document.createTextNode(item.text); textContainer.appendChild(text); li.appendChild(boxSpan); li.appendChild(textContainer); ul.appendChild(li); } else { const list_element = ul.querySelector(`li.legend-element:nth-of-type(${item.datasetIndex+1})`); const boxSpan = list_element.querySelector("span"); const textContainer = list_element.querySelector("p"); boxSpan.style.background = item.fillStyle; boxSpan.style.borderColor = item.strokeStyle; boxSpan.style.borderWidth = item.lineWidth + 'px'; textContainer.style.color = item.fontColor; textContainer.style.textDecoration = item.hidden ? 'line-through' : ''; } }); }, }; 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 const tooltip_plugin = { callbacks: { label: function (context) { 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"]) }, title: items => { let ms = items[0].raw.x; let options = { weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" } return new Date(ms).toLocaleDateString("de-DE", options); }, beforeTitle: function (context) { return context[0].dataset.label; }, } }; await fetch(`get.php`, { method: "GET", }) .then(res => res.json()) .then(result => { player_entries = result["entries"]; player_accounts = result["accounts"]; 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 minval = -1; let maxval = -1; 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 timestamp in player_entries[puuid]) { // Für alle Player Entries Punktzahl und Punktedifferenz berechnen // Bei der Gelegenheit auch Entries in player_entries_byName eintragen const current_points = rank_to_points(player_entries[puuid][timestamp]["tier"], player_entries[puuid][timestamp]["rank"], player_entries[puuid][timestamp]["points"]); const adj_timestamp = parseInt(timestamp) * 1000; player_rank_values[puuid].push({x: adj_timestamp, 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: adj_timestamp, y: 0}); } else { player_progress_values[puuid].push({ x: adj_timestamp, y: current_points - player_start_points }); } if (minval === -1 || adj_timestamp < minval) { minval = adj_timestamp; } if (maxval === -1 || adj_timestamp > maxval) { maxval = adj_timestamp; } } // Linie für den Spieler zu Datasets des Graphen hinzufügen all_rank_yDatasets.push({ label: `${player_accounts[puuid]["gameName"]}#${player_accounts[puuid]["tagLine"]}`, fill: false, borderColor: getColor(color_counter), backgroundColor: getColor(color_counter), borderJoinStyle: "round", data: player_rank_values[puuid], spanGaps: true, pointHitRadius: 16, pointRadius: 0, }) all_progress_yDatasets.push({ label: `${player_accounts[puuid]["gameName"]}#${player_accounts[puuid]["tagLine"]}`, fill: false, borderColor: getColor(color_counter), backgroundColor: getColor(color_counter), borderJoinStyle: "round", data: player_progress_values[puuid], spanGaps: true, pointHitRadius: 16, pointRadius: 0, }) color_counter++; } // Graphen erstellen combined_charts.push(new Chart(`progress-chart-combined`, { type: "line", data: { datasets: all_rank_yDatasets, }, options: { plugins: { tooltip: tooltip_plugin, zoom: {...zoom_plugin}, legend: { display: false, }, htmlLegend: { containerID: 'combined-chart-legend', } }, scales: { x: x_scale, y: { display: true, stepSize: 100, ticks: { callback: (value) => { return points_to_rankstring(value, false); } } }, }, responsive: true, maintainAspectRatio: false, }, plugins: [htmlLegendPlugin], })); combined_charts.push(new Chart(`progress-chart-combined-progress`, { type: "line", data: { datasets: all_progress_yDatasets, }, options: { plugins: { tooltip: tooltip_plugin, zoom: {...zoom_plugin}, legend: { display: false, }, htmlLegend: { containerID: 'combined-chart-legend', } }, scales: { x: x_scale, y: { display: true, }, }, responsive: true, maintainAspectRatio: false, }, plugins: [htmlLegendPlugin], })); minval -= 7200000; maxval += 7200000; combined_charts.forEach(combined_chart => { combined_chart.options.plugins.zoom.limits = { x: {min: minval, max: maxval}, } }) }) .catch(e => console.error(e)) const charts = document.getElementsByClassName("progress-chart"); for (const chart of charts) { let puuid = chart.id.split("-"); puuid.splice(0, 2); puuid = puuid.join("-"); let values = []; let minval = Object.keys(player_entries[puuid])[0] * 1000 - 7200000; let maxval = Object.keys(player_entries[puuid])[Object.keys(player_entries[puuid]).length - 1] * 1000 + 7200000; 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"]); values.push({x: parseInt(entriesKey) * 1000, y: points}); } const player_chart = new Chart(`progress-chart-${puuid}`, { type: "line", data: { datasets: [{ label: `${player_accounts[puuid]["gameName"]}#${player_accounts[puuid]["tagLine"]}`, fill: false, borderColor: "rgba(150,150,175)", backgroundColor: "rgba(150,150,175)", borderJoinStyle: "round", data: values, pointHitRadius: 16, pointRadius: 0, }] }, options: { plugins: { legend: {display: false}, tooltip: tooltip_plugin, zoom: {...zoom_plugin}, }, scales: { x: x_scale, y: { display: true, stepSize: 100, ticks: { callback: (value) => { return points_to_rankstring(value, false); } } }, }, responsive: true, maintainAspectRatio: false, } }); player_chart.options.plugins.zoom.limits = { x: {min: minval, max: maxval}, } player_charts.push(player_chart); } } function rank_to_points(tier, rank, lp) { const apex_tiers = (tier === "MASTER" || tier === "GRANDMASTER" || tier === "CHALLENGER"); const tiers = { "DIAMOND": 2400, "EMERALD": 2000, "PLATINUM": 1600, "GOLD": 1200, "SILVER": 800, "BRONZE": 400, "IRON": 0, }; const ranks = { "I": 300, "II": 200, "III": 100, "IV": 0, }; if (apex_tiers) { return 2800 + lp; } else { return tiers[tier] + ranks[rank] + lp; } } function points_to_rankstring(points, include_LP = true) { const apex_tiers = (points >= 2800); let lp = (apex_tiers) ? points - 2800 : points % 100 let rank = (points - lp) % 400; let tier = (points - lp - rank); const tiers = { 2400: "Diamond", 2000: "Emerald", 1600: "Platinum", 1200: "Gold", 800: "Silver", 400: "Bronze", 0: "Iron", }; const ranks = { 300: "I", 200: "II", 100: "III", 0: "IV", }; let rank_string = (apex_tiers) ? "Master" : tiers[tier]; if (!apex_tiers) rank_string += ` ${ranks[rank]}`; if (include_LP || apex_tiers) rank_string += ` ${lp} LP`; return rank_string; } function format_rank(tier, rank, lp) { tier = tier.charAt(0).toUpperCase() + tier.slice(1).toLowerCase(); const apex_tiers = (tier === "Master" || tier === "Grandmaster" || tier === "Challenger"); let rank_string = tier; if (!apex_tiers) rank_string += ` ${rank}`; rank_string += ` ${lp} LP`; return rank_string; } function getColor(num) { const colors = ["#33b1ff", "#d2a106", "#007d79", "#8a3ffc", "#ff7eb6", "#ba4e00", "#fa4d56", "#fff1f1", "#6fdc8c", "#4589ff", "#d12771", "#08bdba", "#bae6ff", "#d4bbff"]; return colors[num % 9]; } function toggle_combined_chart() { const comb_charts = this.parentNode.parentNode.querySelectorAll(`.leaderboard>.graph-wrapper`); const chart = this.parentNode.parentNode.querySelector(`.graph-wrapper.${this.id}`); const buttons = document.querySelectorAll("button.open-general-graph"); if (chart.classList.contains("closed")) { comb_charts.forEach(element => element.classList.add("closed")); buttons.forEach(element => element.classList.remove("dropdown-open")); chart.classList.remove("closed"); this.classList.add("dropdown-open"); document.querySelector("#combined-chart-legend").classList.remove("hidden"); } else { chart.classList.add("closed"); this.classList.remove("dropdown-open"); document.querySelector("#combined-chart-legend").classList.add("hidden"); } } 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("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 => { eventSource.close(); let response = JSON.parse(e.data); const currenttime = new Date(); const updatetime = new Date(response.last); let timediff = new Date(currenttime - updatetime); let resttime = new Date( 1000 * 60 * response.limit - (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.onerror = e => { eventSource.close(); window.alert(`Beim Update ist ein Fehler aufgetreten. Versuche es später noch einmal`) reset_button(this); } eventSource.addEventListener("done", e => { eventSource.close(); reset_button(this); update_leaderboard_elements(); }) async function reset_button(button) { await new Promise(r => setTimeout(r,400)); // warten bis ladebalken am ende angekommen ist button.style.setProperty("--button-loading-bar-opacity", `0`); await new Promise(r => setTimeout(r,400)); // warten bis ladebalken verblasst ist button.style.setProperty("--button-loading-bar-width", `0`); await new Promise(r => setTimeout(r,400)); // warten bis ladebalken wieder am anfang angekommen ist button.style.setProperty("--button-loading-bar-opacity", `1`); button.disabled = false; 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("mousedown", toggle_leaderboard_chart)); // recreate charts create_charts(); }) .catch(e => console.error(e)); }