roi.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. const profits: Promise<Profit[]> = (async function () {
  2. const response = await fetch('roi.json');
  3. return await response.json();
  4. })();
  5. const lowVolume = document.querySelector('#low-volume') as HTMLInputElement;
  6. async function render() {
  7. const formatDecimal = new Intl.NumberFormat(undefined,
  8. {maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: 'lessPrecision'}).format;
  9. const formatWhole = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format;
  10. const tbody = document.querySelector('tbody')!;
  11. tbody.innerHTML = '';
  12. for (const p of await profits) {
  13. const volumeRatio = p.output_per_day / p.average_traded_7d;
  14. if (!lowVolume.checked && volumeRatio > 0.05) {
  15. continue;
  16. }
  17. const tr = document.createElement('tr');
  18. const profit_per_area = p.profit_per_day / p.area;
  19. const break_even = p.profit_per_day > 0 ? p.capex / p.profit_per_day : Infinity;
  20. tr.innerHTML = `
  21. <td>${p.output}</td>
  22. <td>${p.expertise}</td>
  23. <td style="color: ${color(profit_per_area, 0, 250)}">${formatDecimal(profit_per_area)}</td>
  24. <td><span style="color: ${color(break_even, 30, 3)}">${formatDecimal(break_even)}</span>d</td>
  25. <td style="color: ${color(p.capex, 300_000, 40_000)}">${formatWhole(p.capex)}</td>
  26. <td style="color: ${color(p.cost_per_day, 40_000, 1_000)}">${formatWhole(p.cost_per_day)}</td>
  27. <td style="color: ${color(p.logistics_per_area, 2, 0.2)}">${formatDecimal(p.logistics_per_area)}</td>
  28. <td>
  29. ${formatDecimal(p.output_per_day)}<br>
  30. <span style="color: ${color(volumeRatio, 0.05, 0.002)}">${formatWhole(p.average_traded_7d)}</span>
  31. </td>
  32. `;
  33. const output = tr.querySelector('td')!;
  34. output.dataset.tooltip = p.recipe;
  35. tbody.appendChild(tr);
  36. }
  37. }
  38. function color(n: number, low: number, high: number): string {
  39. // scale n from low..high to 0..1 clamped
  40. const scale = Math.min(Math.max((n - low) / (high - low), 0), 1);
  41. return `color-mix(in oklch, #0c8 ${scale * 100}%, #f70)`;
  42. }
  43. const main = document.querySelector('main')!;
  44. const popover = document.querySelector('#popover') as HTMLElement;
  45. main.addEventListener('mouseover', (event) => {
  46. const target = event.target as HTMLElement;
  47. if (target.dataset.tooltip) {
  48. popover.textContent = target.dataset.tooltip;
  49. const rect = target.getBoundingClientRect();
  50. popover.style.left = `${rect.left}px`;
  51. popover.style.top = `${rect.bottom}px`;
  52. popover.showPopover();
  53. }
  54. });
  55. main.addEventListener('mouseout', (event) => {
  56. const target = event.target as HTMLElement;
  57. if (target.dataset.tooltip)
  58. popover.hidePopover();
  59. });
  60. lowVolume.addEventListener('change', render);
  61. render();
  62. interface Profit {
  63. output: string
  64. recipe: string
  65. expertise: string
  66. profit_per_day: number
  67. area: number
  68. capex: number
  69. cost_per_day: number
  70. logistics_per_area: number
  71. output_per_day: number
  72. average_traded_7d: number
  73. }