roi.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. // ts/popover.ts
  2. function setupPopover() {
  3. const main = document.querySelector("main");
  4. const popover = document.querySelector("#popover");
  5. main.addEventListener("mouseover", (event) => {
  6. const target = event.target;
  7. if (target.dataset.tooltip) {
  8. popover.textContent = target.dataset.tooltip;
  9. const rect = target.getBoundingClientRect();
  10. popover.style.left = `${rect.left}px`;
  11. popover.style.top = `${rect.bottom}px`;
  12. popover.showPopover();
  13. }
  14. });
  15. main.addEventListener("mouseout", (event) => {
  16. const target = event.target;
  17. if (target.dataset.tooltip)
  18. popover.hidePopover();
  19. });
  20. }
  21. // ts/roi.ts
  22. var roiCache = {};
  23. async function getROI(cx) {
  24. const response = await fetch(`/roi_${cx.toLowerCase()}.json`);
  25. const lastModified = new Date(response.headers.get("last-modified"));
  26. const profits = await response.json();
  27. return { lastModified, profits };
  28. }
  29. var lowVolume = document.querySelector("input#low-volume");
  30. var cxSelect = document.querySelector("select#cx");
  31. var expertise = {
  32. AGRICULTURE: "agri",
  33. CHEMISTRY: "chem",
  34. CONSTRUCTION: "const",
  35. ELECTRONICS: "elec",
  36. FOOD_INDUSTRIES: "food ind",
  37. FUEL_REFINING: "fuel",
  38. MANUFACTURING: "mfg",
  39. METALLURGY: "metal",
  40. RESOURCE_EXTRACTION: "res ext"
  41. };
  42. var expertiseSelect = document.querySelector("select#expertise");
  43. for (const key of Object.keys(expertise)) {
  44. const option = document.createElement("option");
  45. option.value = key;
  46. option.textContent = key.replace("_", " ").toLowerCase();
  47. expertiseSelect.appendChild(option);
  48. }
  49. var buildingSelect = document.querySelector("select#building");
  50. var formatDecimal = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: "lessPrecision" }).format;
  51. var formatWhole = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format;
  52. var currentSortKey = "break_even";
  53. var currentSortAsc = true;
  54. var headersInitialized = false;
  55. async function render() {
  56. const tbody = document.querySelector("tbody");
  57. tbody.innerHTML = "";
  58. const cx = cxSelect.value;
  59. if (!roiCache[cx])
  60. roiCache[cx] = getROI(cx);
  61. const { lastModified, profits } = await roiCache[cx];
  62. if (!headersInitialized) {
  63. const ths = document.querySelectorAll("th");
  64. const keys = [
  65. "outputs",
  66. "expertise",
  67. "profit_per_area",
  68. "break_even",
  69. "capex",
  70. "cost_per_day",
  71. "logistics_per_area",
  72. "market_capacity_area"
  73. ];
  74. ths.forEach((th, i) => {
  75. if (keys[i]) {
  76. th.style.cursor = "pointer";
  77. th.title = "Click to sort";
  78. th.addEventListener("click", () => {
  79. if (currentSortKey === keys[i]) {
  80. currentSortAsc = !currentSortAsc;
  81. } else {
  82. currentSortKey = keys[i];
  83. currentSortAsc = keys[i] === "break_even" ? true : false;
  84. }
  85. render();
  86. });
  87. }
  88. });
  89. headersInitialized = true;
  90. }
  91. const buildingTickers = new Set(profits.map((p) => p.building));
  92. 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));
  93. let selectedBuilding = buildingSelect.value;
  94. let buildingFound = false;
  95. buildingSelect.innerHTML = '<option value="">(all)</option>';
  96. for (const building of buildings)
  97. if (expertiseSelect.value === "" || expertiseSelect.value === building.expertise) {
  98. const option = document.createElement("option");
  99. option.value = building.ticker;
  100. option.textContent = building.ticker;
  101. if (building.ticker === selectedBuilding) {
  102. buildingFound = true;
  103. option.selected = true;
  104. }
  105. buildingSelect.appendChild(option);
  106. }
  107. if (!buildingFound)
  108. selectedBuilding = "";
  109. const filteredProfits = profits.filter((p) => {
  110. const volumeRatio = p.output_per_day / p.average_traded_7d;
  111. if (!lowVolume.checked && volumeRatio > 0.05)
  112. return false;
  113. if (expertiseSelect.value !== "" && p.expertise !== expertiseSelect.value)
  114. return false;
  115. if (selectedBuilding !== "" && p.building !== selectedBuilding)
  116. return false;
  117. return true;
  118. });
  119. filteredProfits.sort((a, b) => {
  120. let valA = a[currentSortKey];
  121. let valB = b[currentSortKey];
  122. if (currentSortKey === "outputs") {
  123. valA = a.outputs.map((o) => o.ticker).join(", ");
  124. valB = b.outputs.map((o) => o.ticker).join(", ");
  125. }
  126. if (valA < valB)
  127. return currentSortAsc ? -1 : 1;
  128. if (valA > valB)
  129. return currentSortAsc ? 1 : -1;
  130. return 0;
  131. });
  132. for (const p of filteredProfits) {
  133. const tr = document.createElement("tr");
  134. tr.innerHTML = `
  135. <td>${p.outputs.map((o) => o.ticker).join(", ")}</td>
  136. <td>${expertise[p.expertise]}</td>
  137. <td style="color: ${color(p.profit_per_area, 0, 300)}">${formatDecimal(p.profit_per_area)}</td>
  138. <td><span style="color: ${color(p.break_even, 30, 3)}">${formatDecimal(p.break_even)}</span>d</td>
  139. <td style="color: ${color(p.capex, 300000, 40000)}">${formatWhole(p.capex)}</td>
  140. <td style="color: ${color(p.cost_per_day, 40000, 1000)}">${formatWhole(p.cost_per_day)}</td>
  141. <td style="color: ${color(p.logistics_per_area, 2, 0.2)}">${formatDecimal(p.logistics_per_area)}</td>
  142. <td style="color: ${color(p.market_capacity_area, 20, 500)}">${formatWhole(p.market_capacity_area)}</td>
  143. `;
  144. const output = tr.querySelector("td");
  145. output.dataset.tooltip = p.recipe;
  146. const profitCell = tr.querySelectorAll("td")[2];
  147. const revenue = p.outputs.reduce((sum, o) => sum + o.amount * o.vwap_7d, 0);
  148. const inputCost = p.input_costs.reduce((sum, o) => sum + o.amount * o.vwap_7d, 0);
  149. profitCell.dataset.tooltip = formatMatPrices(p.outputs) + `
  150. ` + formatMatPrices(p.input_costs) + `
  151. ` + "worker consumables: " + formatWhole(p.worker_consumable_cost_per_day) + `
  152. ` + `(${formatWhole(revenue)} - ${formatWhole(inputCost)}) × ${formatDecimal(p.runs_per_day)} runs ` + `- ${formatWhole(p.worker_consumable_cost_per_day)} = ${formatDecimal(p.profit_per_day)}
  153. ` + `${formatDecimal(p.profit_per_day)} / ${formatWhole(p.area)} area = ${formatDecimal(p.profit_per_area)}`;
  154. const marketCell = tr.querySelectorAll("td")[7];
  155. 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`;
  156. tbody.appendChild(tr);
  157. }
  158. document.getElementById("last-updated").textContent = `last updated: ${lastModified.toLocaleString(undefined, { dateStyle: "full", timeStyle: "long", hour12: false })}`;
  159. }
  160. function color(n, low, high) {
  161. const scale = Math.min(Math.max((n - low) / (high - low), 0), 1);
  162. return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`;
  163. }
  164. function formatMatPrices(matPrices) {
  165. return matPrices.map(({ ticker, amount, vwap_7d }) => `${ticker}: ${amount} × ${formatDecimal(vwap_7d)} = ${formatWhole(amount * vwap_7d)}`).join(`
  166. `);
  167. }
  168. setupPopover();
  169. lowVolume.addEventListener("change", render);
  170. cxSelect.addEventListener("change", render);
  171. expertiseSelect.addEventListener("change", render);
  172. buildingSelect.addEventListener("change", render);
  173. render();
  174. //# debugId=360E975B4A4AE32464756E2164756E21