// ts/popover.ts function setupPopover() { const main = document.querySelector("main"); const popover = document.querySelector("#popover"); main.addEventListener("mouseover", (event) => { const target = event.target; if (target.dataset.tooltip) { popover.textContent = target.dataset.tooltip; const rect = target.getBoundingClientRect(); popover.style.left = `${rect.left}px`; popover.style.top = `${rect.bottom}px`; popover.showPopover(); } }); main.addEventListener("mouseout", (event) => { const target = event.target; if (target.dataset.tooltip) popover.hidePopover(); }); } // ts/roi.ts var roiCache = {}; async function getROI(cx) { const response = await fetch(`/roi_${cx.toLowerCase()}.json`); const lastModified = new Date(response.headers.get("last-modified")); const profits = await response.json(); return { lastModified, profits }; } var lowVolume = document.querySelector("input#low-volume"); var cxSelect = document.querySelector("select#cx"); var expertise = { AGRICULTURE: "agri", CHEMISTRY: "chem", CONSTRUCTION: "const", ELECTRONICS: "elec", FOOD_INDUSTRIES: "food ind", FUEL_REFINING: "fuel", MANUFACTURING: "mfg", METALLURGY: "metal", RESOURCE_EXTRACTION: "res ext" }; var expertiseSelect = document.querySelector("select#expertise"); for (const key of Object.keys(expertise)) { const option = document.createElement("option"); option.value = key; option.textContent = key.replace("_", " ").toLowerCase(); expertiseSelect.appendChild(option); } var buildingSelect = document.querySelector("select#building"); var formatDecimal = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: "lessPrecision" }).format; var formatWhole = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format; var currentSortKey = "break_even"; var currentSortAsc = true; var headersInitialized = false; async function render() { const tbody = document.querySelector("tbody"); tbody.innerHTML = ""; const cx = cxSelect.value; if (!roiCache[cx]) roiCache[cx] = getROI(cx); const { lastModified, profits } = await roiCache[cx]; if (!headersInitialized) { const ths = document.querySelectorAll("th"); const keys = [ "outputs", "expertise", "profit_per_area", "break_even", "capex", "cost_per_day", "logistics_per_area", "market_capacity_area" ]; ths.forEach((th, i) => { if (keys[i]) { th.style.cursor = "pointer"; th.title = "Click to sort"; th.addEventListener("click", () => { if (currentSortKey === keys[i]) { currentSortAsc = !currentSortAsc; } else { currentSortKey = keys[i]; currentSortAsc = keys[i] === "break_even" ? true : false; } render(); }); } }); headersInitialized = true; } const buildingTickers = new Set(profits.map((p) => p.building)); const buildings = Array.from(buildingTickers).map((building) => ({ ticker: building, expertise: profits.find((p) => p.building === building).expertise })).sort((a, b) => a.ticker.localeCompare(b.ticker)); let selectedBuilding = buildingSelect.value; let buildingFound = false; buildingSelect.innerHTML = ''; for (const building of buildings) if (expertiseSelect.value === "" || expertiseSelect.value === building.expertise) { const option = document.createElement("option"); option.value = building.ticker; option.textContent = building.ticker; if (building.ticker === selectedBuilding) { buildingFound = true; option.selected = true; } buildingSelect.appendChild(option); } if (!buildingFound) selectedBuilding = ""; const filteredProfits = profits.filter((p) => { const volumeRatio = p.output_per_day / p.average_traded_7d; if (!lowVolume.checked && volumeRatio > 0.05) return false; if (expertiseSelect.value !== "" && p.expertise !== expertiseSelect.value) return false; if (selectedBuilding !== "" && p.building !== selectedBuilding) return false; return true; }); filteredProfits.sort((a, b) => { let valA = a[currentSortKey]; let valB = b[currentSortKey]; if (currentSortKey === "outputs") { valA = a.outputs.map((o) => o.ticker).join(", "); valB = b.outputs.map((o) => o.ticker).join(", "); } if (valA < valB) return currentSortAsc ? -1 : 1; if (valA > valB) return currentSortAsc ? 1 : -1; return 0; }); for (const p of filteredProfits) { const tr = document.createElement("tr"); tr.innerHTML = `