roi.ts 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import {setupPopover} from './popover';
  2. const roi: Promise<{lastModified: Date, profits: Profit[]}> = (async function () {
  3. const response = await fetch('/roi.json');
  4. const lastModified = new Date(response.headers.get('last-modified')!);
  5. const profits = await response.json();
  6. return {lastModified, profits};
  7. })();
  8. const lowVolume = document.querySelector('input#low-volume') as HTMLInputElement;
  9. const expertise = {
  10. AGRICULTURE: 'agri',
  11. CHEMISTRY: 'chem',
  12. CONSTRUCTION: 'const',
  13. ELECTRONICS: 'elec',
  14. FOOD_INDUSTRIES: 'food ind',
  15. FUEL_REFINING: 'fuel',
  16. MANUFACTURING: 'mfg',
  17. METALLURGY: 'metal',
  18. RESOURCE_EXTRACTION: 'res ext',
  19. } as const;
  20. const expertiseSelect = document.querySelector('select#expertise') as HTMLSelectElement;
  21. for (const key of Object.keys(expertise)) {
  22. const option = document.createElement('option');
  23. option.value = key;
  24. option.textContent = key.replace('_', ' ').toLowerCase();
  25. expertiseSelect.appendChild(option);
  26. }
  27. async function render() {
  28. const formatDecimal = new Intl.NumberFormat(undefined,
  29. {maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: 'lessPrecision'}).format;
  30. const formatWhole = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format;
  31. const tbody = document.querySelector('tbody')!;
  32. tbody.innerHTML = '';
  33. const {lastModified, profits} = await roi;
  34. for (const p of profits) {
  35. const volumeRatio = p.output_per_day / p.average_traded_7d;
  36. if (!lowVolume.checked && volumeRatio > 0.05)
  37. continue;
  38. if (expertiseSelect.value !== '' && p.expertise !== expertiseSelect.value)
  39. continue;
  40. const tr = document.createElement('tr');
  41. const profit_per_area = p.profit_per_day / p.area;
  42. const break_even = p.profit_per_day > 0 ? p.capex / p.profit_per_day : Infinity;
  43. tr.innerHTML = `
  44. <td>${p.output}</td>
  45. <td>${expertise[p.expertise]}</td>
  46. <td style="color: ${color(profit_per_area, 0, 250)}">${formatDecimal(profit_per_area)}</td>
  47. <td><span style="color: ${color(break_even, 30, 3)}">${formatDecimal(break_even)}</span>d</td>
  48. <td style="color: ${color(p.capex, 300_000, 40_000)}">${formatWhole(p.capex)}</td>
  49. <td style="color: ${color(p.cost_per_day, 40_000, 1_000)}">${formatWhole(p.cost_per_day)}</td>
  50. <td style="color: ${color(p.logistics_per_area, 2, 0.2)}">${formatDecimal(p.logistics_per_area)}</td>
  51. <td>
  52. ${formatDecimal(p.output_per_day)}<br>
  53. <span style="color: ${color(volumeRatio, 0.05, 0.002)}">${formatWhole(p.average_traded_7d)}</span>
  54. </td>
  55. `;
  56. const output = tr.querySelector('td')!;
  57. output.dataset.tooltip = p.recipe;
  58. tbody.appendChild(tr);
  59. }
  60. document.getElementById('last-updated')!.textContent =
  61. `last updated: ${lastModified.toLocaleString(undefined, {dateStyle: 'full', timeStyle: 'long', hour12: false})}`;
  62. }
  63. function color(n: number, low: number, high: number): string {
  64. // scale n from low..high to 0..1 clamped
  65. const scale = Math.min(Math.max((n - low) / (high - low), 0), 1);
  66. return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`;
  67. }
  68. setupPopover();
  69. lowVolume.addEventListener('change', render);
  70. expertiseSelect.addEventListener('change', render);
  71. render();
  72. interface Profit {
  73. output: string
  74. recipe: string
  75. expertise: keyof typeof expertise
  76. profit_per_day: number
  77. area: number
  78. capex: number
  79. cost_per_day: number
  80. logistics_per_area: number
  81. output_per_day: number
  82. average_traded_7d: number
  83. }