"use strict"; // Hent canvas-context const ctx = document.getElementById("chart").getContext("2d"); const toggleBtn = document.getElementById("toggle-axis"); // Hjelpefunksjon for tilfeldig farge function getRandomColor() { const r = Math.floor(Math.random() * 200 + 30); const g = Math.floor(Math.random() * 200 + 30); const b = Math.floor(Math.random() * 200 + 30); return `rgb(${r}, ${g}, ${b})`; } // Dynamically combine selectedClubs and clubs from selectedLeagues // function getAllSelectedClubs() { // const leagueClubIds = new Set( // selectedLeagues // .flatMap((league) => league.clubs || []) // .map((club) => club.id) // ); // return selectedClubs // .filter((club) => !leagueClubIds.has(club.id)) // .concat(selectedLeagues.flatMap((league) => league.clubs || [])); // } /** * Lager dataset avhengig av currentMode */ function buildDatasets() { // const selectedClubs = getAllSelectedClubs(); return selectedClubs.map((club) => { const img = new Image(); img.src = `https://file.mackolikfeeds.com/teams/${club.contestantId}`; img.onerror = () => { img.src = `https://omo.akamai.opta.net/image.php?secure=true&h=omo.akamai.opta.net&sport=football&entity=team&description=badges&dimensions=150&id=${club.contestantId}`; }; // NÃ¥r bildet er lastet, tegn pÃ¥ nytt img.onload = () => { myChart.update("none"); }; const data = club.ratings.map((r) => { const d = new Date(r.ratingDate); const localMidnight = new Date( d.getFullYear(), d.getMonth(), d.getDate() ).getTime(); return { x: localMidnight, y: currentMode === "ranking" ? r.ranking : r.currentRating, }; }); return { label: club.name, data, fill: false, borderColor: getRandomColor(), tension: 0, logo: img, }; }); } // Plugin for drawing logos pÃ¥ enden av hver linje const logoPlugin = { id: "logoPlugin", afterDraw: (chart) => { const { ctx, scales, chartArea } = chart; const xScale = scales.x; const yScale = scales.y; const domainMax = xScale.max; const pixelX = xScale.getPixelForValue(domainMax); chart.data.datasets.forEach((dataset) => { const pts = dataset.data; if (!dataset.logo.complete || pts.length < 2) return; // Finn to datapunkter rundt domainMax (samme som før) … let a = null, b = null; for (let i = 0; i < pts.length - 1; i++) { if (pts[i].x <= domainMax && pts[i + 1].x >= domainMax) { a = pts[i]; b = pts[i + 1]; break; } } if (!a) { a = pts[pts.length - 2]; b = pts[pts.length - 1]; } const t = (domainMax - a.x) / (b.x - a.x); const interpolatedY = a.y + t * (b.y - a.y); const pixelY = yScale.getPixelForValue(interpolatedY); // Sjekk at punktet ligger innenfor chartArea const { left, top, right, bottom } = chartArea; if ( pixelX < left || pixelX > right || pixelY < top || pixelY > bottom ) { // utenfor – hopp over tegningen return; } // Tegn logo const size = 24; ctx.drawImage( dataset.logo, pixelX - size / 2, pixelY - size / 2, size, size ); }); }, }; // Opprett Chart.js-grafen const myChart = new Chart(ctx, { type: "line", data: { datasets: [] }, // Start with empty datasets options: { responsive: true, maintainAspectRatio: false, animation: false, plugins: { title: { display: true, text: "Historical Opta Power Rankings" }, tooltip: { mode: "nearest", intersect: false, callbacks: { label: function (context) { // 1) Prøv først parsed.y (sikreste vei til tallet) let value = context.parsed?.y; // 2) Hvis parsed.y ikke finnes, plukk ut raw.y eller raw direkte if (value === undefined) { const raw = context.raw; value = raw != null && typeof raw === "object" && raw.y != null ? raw.y : raw; } // 3) Sørg for at det virkelig er et tall value = Number(value); // 4) Antall desimaler const decimals = currentMode === "ranking" ? 0 : 3; // 5) Sett label om du ønsker const label = context.dataset.label ? context.dataset.label + ": " : ""; // 6) Returner formatert string return label + value.toFixed(decimals); }, }, }, legend: { display: false }, }, scales: { x: { type: "time", time: { unit: "day", tooltipFormat: "yyyy-MM-dd" }, title: { display: false, text: "Time" }, }, y: { reverse: currentMode === "ranking", beginAtZero: false, title: { display: true, text: currentMode === "ranking" ? "Ranking (1 = best)" : "Rating", }, ticks: { precision: currentMode === "ranking" ? 0 : 2 }, }, }, }, plugins: [logoPlugin], }); // Oppdater tekst pÃ¥ toggle-knappen function updateToggleText() { toggleBtn.textContent = currentMode === "ranking" ? "Show rating" : "Show ranking"; } function toggleAxis() { // 1) Bytt mode currentMode = currentMode === "ranking" ? "rating" : "ranking"; const params = new URLSearchParams(window.location.search); params.set("mode", currentMode); history.replaceState( null, "", `${window.location.pathname}?${params.toString()}` ); // Set cookie for mode document.cookie = `mode=${currentMode}; path=/historical-opta-power-rankings; SameSite=Strict`; updateChartForMode(); } function updateChartForMode() { // 2) Bytt dataset og oppdater tittel & reversering myChart.options.scales.y.reverse = currentMode === "ranking"; myChart.options.scales.y.title.text = currentMode === "ranking" ? "Ranking (1 = best)" : "Rating"; // 3) Juster ticks‑innstillingene for desimaler if (currentMode === "rating") { myChart.options.scales.y.ticks = { precision: 2, callback: (val) => val.toFixed(2), }; } else { myChart.options.scales.y.ticks = { precision: 0, }; } // 1) Oppdater datasett og la Chart.js autoskalere myChart.data.datasets = buildDatasets(); delete myChart.options.scales.x.min; delete myChart.options.scales.x.max; delete myChart.options.scales.y.min; delete myChart.options.scales.y.max; myChart.update("none"); // 2) Hent nye min/max fra Chart.js-skalaene const xScale = myChart.scales.x; const yScale = myChart.scales.y; const newXMin = xScale.min; const newXMax = xScale.max; const newYMin = yScale.min; const newYMax = yScale.max; // 3) Oppdater x-slideren slik at den spenner over data-domenet xSlider.noUiSlider.updateOptions({ start: [newXMin, newXMax], range: { min: newXMin, max: newXMax }, }); initYSlider(newYMin, newYMax); // 4) Oppdater y-slideren tilsvarende // ySlider.noUiSlider.updateOptions({ // start: [newYMin, newYMax], // range: { min: newYMin, max: newYMax }, // format: wNumb({ // decimals: currentMode === "ranking" ? 0 : 2, // thousand: "", // }), // direction: "rtl", // }); // 6) Oppdater knappetekst updateToggleText(); toggleLoading(false); // ↠spinner er ikke mer nødvendig nÃ¥r grafen er fullstendig oppdatert } // Koble pÃ¥ knapp-lytteren toggleBtn.addEventListener("click", toggleAxis); // Sett initial knappetekst updateToggleText(); const xScale = myChart.scales.x; const yScale = myChart.scales.y; const xMin = xScale.min; const xMax = xScale.max; const yMin = yScale.min; const yMax = yScale.max; // X‑akseâ€slider (datoer) const xSlider = document.getElementById("x-range"); noUiSlider.create(xSlider, { start: [xMin, xMax], connect: true, behaviour: "drag", // ↠her! range: { min: xMin, max: xMax }, //tooltips: [wNumb({ decimals: 0 }), wNumb({ decimals: 0 })], }); // NÃ¥r brukeren drar: oppdater chartets x‑min og x‑max xSlider.noUiSlider.on("update", (values) => { myChart.options.scales.x.min = +values[0]; myChart.options.scales.x.max = +values[1]; myChart.update("none"); // “none†for jevn oppdatering uten animasjon }); const ySlider = document.getElementById("y-range"); initYSlider(yMin, yMax); function initYSlider(min, max) { // 1) Hvis slider allerede eksisterer, fjern den if (ySlider.noUiSlider) { ySlider.noUiSlider.destroy(); } // 2) (Valgfritt) Reset eventuelle inline-stiler: ySlider.innerHTML = ""; // 3) Lag slider pÃ¥ nytt med oppdatert direction noUiSlider.create(ySlider, { start: [min, max], connect: true, behaviour: "drag", orientation: "vertical", direction: currentMode === "ranking" ? "ltr" : "rtl", range: { min, max }, format: wNumb({ decimals: currentMode === "ranking" ? 0 : 2, thousand: "", }), }); // 4) Legg pÃ¥ event-listener for oppdatering ySlider.noUiSlider.on("update", (values) => { myChart.options.scales.y.min = +values[0]; myChart.options.scales.y.max = +values[1]; myChart.update("none"); }); updateYSliderButtons(); } // Y‑akseâ€slider (ranking) // const ySlider = document.getElementById("y-range"); // noUiSlider.create(ySlider, { // start: [yMin, yMax], // connect: true, // behaviour: "drag", // ↠her! // orientation: "vertical", // direction: currentMode === "ranking" ? "ltr" : "rtl", // range: { min: yMin, max: yMax }, // format: wNumb({ // decimals: currentMode === "ranking" ? 0 : 2, // thousand: "", // }), // }); // ySlider.noUiSlider.on("update", (values) => { // myChart.options.scales.y.min = +values[0]; // myChart.options.scales.y.max = +values[1]; // myChart.update("none"); // }); window.addEventListener("DOMContentLoaded", () => { initializeSSE(streamId); }); function initializeSSE(streamId) { const params = new URLSearchParams(window.location.search); const clubIds = params.get("club-ids") || ""; const leagueIds = params.get("league-ids") || ""; if (params.has("mode")) { currentMode = params.get("mode"); } const url = `/historical-opta-power-rankings/api/streamSelections` + // Fix duplicate path `?id=${streamId}`; const evtSource = new EventSource(url); toggleLoading(true); // Show spinner evtSource.addEventListener("initialSelection", (e) => { const payload = JSON.parse(e.data); selectedClubs = payload.selectedClubs; updateChartForMode(); evtSource.close(); }); } function updateYSliderButtons() { const lowerHandle = document.querySelector(".y-slider .noUi-handle-lower"); const upperHandle = document.querySelector(".y-slider .noUi-handle-upper"); if (currentMode === "ranking") { lowerHandle.style.marginBottom = "-15px"; upperHandle.style.marginBottom = "-45px"; } else { lowerHandle.style.marginBottom = "-45px"; upperHandle.style.marginBottom = "-15px"; } }