// 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 = ` ${p.outputs.map((o) => o.ticker).join(", ")} ${expertise[p.expertise]} ${formatDecimal(p.profit_per_area)} ${formatDecimal(p.break_even)}d ${formatWhole(p.capex)} ${formatWhole(p.cost_per_day)} ${formatDecimal(p.logistics_per_area)} ${formatWhole(p.market_capacity_area)} `; const output = tr.querySelector("td"); output.dataset.tooltip = p.recipe; const profitCell = tr.querySelectorAll("td")[2]; const revenue = p.outputs.reduce((sum, o) => sum + o.amount * o.vwap_7d, 0); const inputCost = p.input_costs.reduce((sum, o) => sum + o.amount * o.vwap_7d, 0); profitCell.dataset.tooltip = formatMatPrices(p.outputs) + ` ` + formatMatPrices(p.input_costs) + ` ` + "worker consumables: " + formatWhole(p.worker_consumable_cost_per_day) + ` ` + `(${formatWhole(revenue)} - ${formatWhole(inputCost)}) × ${formatDecimal(p.runs_per_day)} runs ` + `- ${formatWhole(p.worker_consumable_cost_per_day)} = ${formatDecimal(p.profit_per_day)} ` + `${formatDecimal(p.profit_per_day)} / ${formatWhole(p.area)} area = ${formatDecimal(p.profit_per_area)}`; const marketCell = tr.querySelectorAll("td")[7]; marketCell.dataset.tooltip = `Market Capacity: ${formatWhole(p.average_traded_7d)} traded/day ÷ ${formatDecimal(p.output_per_day / p.area)} produced/day/area = ${formatWhole(p.market_capacity_area)} equivalent areas`; tbody.appendChild(tr); } document.getElementById("last-updated").textContent = `last updated: ${lastModified.toLocaleString(undefined, { dateStyle: "full", timeStyle: "long", hour12: false })}`; } function color(n, low, high) { const scale = Math.min(Math.max((n - low) / (high - low), 0), 1); return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`; } function formatMatPrices(matPrices) { return matPrices.map(({ ticker, amount, vwap_7d }) => `${ticker}: ${amount} × ${formatDecimal(vwap_7d)} = ${formatWhole(amount * vwap_7d)}`).join(` `); } setupPopover(); lowVolume.addEventListener("change", render); cxSelect.addEventListener("change", render); expertiseSelect.addEventListener("change", render); buildingSelect.addEventListener("change", render); render(); //# debugId=360E975B4A4AE32464756E2164756E21