|
|
@@ -1,31 +1,79 @@
|
|
|
-(async function () {
|
|
|
+const profits: Promise<Profit[]> = (async function () {
|
|
|
const response = await fetch('roi.json');
|
|
|
- const profits: Profit[] = await response.json();
|
|
|
+ return await response.json();
|
|
|
+})();
|
|
|
+
|
|
|
+const lowVolume = document.querySelector('#low-volume') as HTMLInputElement;
|
|
|
|
|
|
- const format = new Intl.NumberFormat(undefined,
|
|
|
- {maximumFractionDigits: 2, maximumSignificantDigits: 7, roundingPriority: 'lessPrecision'}).format;
|
|
|
+async function render() {
|
|
|
+ const formatDecimal = new Intl.NumberFormat(undefined,
|
|
|
+ {maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: 'lessPrecision'}).format;
|
|
|
+ const formatWhole = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format;
|
|
|
const tbody = document.querySelector('tbody')!;
|
|
|
- for (const p of profits) {
|
|
|
+ tbody.innerHTML = '';
|
|
|
+ for (const p of await profits) {
|
|
|
+ const volumeRatio = p.output_per_day / p.average_traded_7d;
|
|
|
+ if (!lowVolume.checked && volumeRatio > 0.05) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
const tr = document.createElement('tr');
|
|
|
+ const profit_per_area = p.profit_per_day / p.area;
|
|
|
+ const break_even = p.profit_per_day > 0 ? p.capex / p.profit_per_day : Infinity;
|
|
|
tr.innerHTML = `
|
|
|
<td>${p.output}</td>
|
|
|
<td>${p.expertise}</td>
|
|
|
- <td>${format(p.profit_per_area)}</td>
|
|
|
- <td>${format(p.capex)}</td>
|
|
|
- <td>${format(p.cost_per_day)}</td>
|
|
|
- <td>${format(p.logistics_per_area)}</td>
|
|
|
+ <td style="color: ${color(profit_per_area, 0, 500)}">${formatDecimal(profit_per_area)}</td>
|
|
|
+ <td><span style="color: ${color(break_even, 30, 2)}">${formatDecimal(break_even)}</span>d</td>
|
|
|
+ <td style="color: ${color(p.capex, 300_000, 50_000)}">${formatWhole(p.capex)}</td>
|
|
|
+ <td style="color: ${color(p.cost_per_day, 40_000, 1_000)}">${formatWhole(p.cost_per_day)}</td>
|
|
|
+ <td style="color: ${color(p.logistics_per_area, 2, 0.2)}">${formatDecimal(p.logistics_per_area)}</td>
|
|
|
+ <td>
|
|
|
+ ${formatDecimal(p.output_per_day)}<br>
|
|
|
+ <span style="color: ${color(volumeRatio, 0.05, 0.002)}">${formatWhole(p.average_traded_7d)}</span>
|
|
|
+ </td>
|
|
|
`;
|
|
|
+ const output = tr.querySelector('td')!;
|
|
|
+ output.dataset.tooltip = p.recipe;
|
|
|
tbody.appendChild(tr);
|
|
|
}
|
|
|
-})();
|
|
|
+}
|
|
|
+
|
|
|
+function color(n: number, low: number, high: number): string {
|
|
|
+ // scale n from low..high to 0..1 clamped
|
|
|
+ const scale = Math.min(Math.max((n - low) / (high - low), 0), 1);
|
|
|
+ return `color-mix(in oklch, #0c8 ${scale * 100}%, #f70)`;
|
|
|
+}
|
|
|
+
|
|
|
+const main = document.querySelector('main')!;
|
|
|
+const popover = document.querySelector('#popover') as HTMLElement;
|
|
|
+main.addEventListener('mouseover', (event) => {
|
|
|
+ const target = event.target as HTMLElement;
|
|
|
+ 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 as HTMLElement;
|
|
|
+ if (target.dataset.tooltip)
|
|
|
+ popover.hidePopover();
|
|
|
+});
|
|
|
+
|
|
|
+lowVolume.addEventListener('change', render);
|
|
|
+render();
|
|
|
|
|
|
interface Profit {
|
|
|
output: string
|
|
|
recipe: string
|
|
|
expertise: string
|
|
|
- profit_per_area: number
|
|
|
+ profit_per_day: number
|
|
|
+ area: number
|
|
|
capex: number
|
|
|
cost_per_day: number
|
|
|
logistics_per_area: number
|
|
|
- low_volume: boolean
|
|
|
+ output_per_day: number
|
|
|
+ average_traded_7d: number
|
|
|
}
|