1
0

9 Commits efd06285ef ... e3abcd248b

Autor SHA1 Mensagem Data
  raylu e3abcd248b supply: exclude ignored materials from weight/volume 1 dia atrás
  raylu cb6e94c6cd roi: use popover for tooltips 2 dias atrás
  raylu 94e168c383 roi: handle degenerate negative profits 2 dias atrás
  raylu ce53ce2d8d roi: more tooltips 3 dias atrás
  raylu 257c1f630a roi: hide low volume 3 dias atrás
  raylu f5a1a27e13 roi: handle negative profit 3 dias atrás
  raylu 74d6a61586 roi: sort by break even 3 dias atrás
  raylu c26cdc24ee roi: colors 3 dias atrás
  raylu 2248b6fbf2 roi: show daily output/traded 4 dias atrás
6 arquivos alterados com 119 adições e 41 exclusões
  1. 1 1
      buy.py
  2. 18 16
      roi.py
  3. 7 5
      supply.py
  4. 60 12
      ts/roi.ts
  5. 9 5
      www/index.html
  6. 24 2
      www/style.css

+ 1 - 1
buy.py

@@ -20,7 +20,7 @@ def main() -> None:
 			headers={'Authorization': config.fio_api_key})]
 	buy: dict[str, int] = collections.defaultdict(int)
 	for planet in planets:
-		for mat, amount in planet.buy_for_target(7).items():
+		for mat, amount in planet.supply_for_days(7).items():
 			buy[mat] += amount
 
 	# what we have

+ 18 - 16
roi.py

@@ -20,13 +20,13 @@ def main() -> None:
 	for recipe in recipes:
 		if profit := calc_profit(recipe, buildings, materials, prices):
 			profits.append(profit)
-	profits.sort(reverse=True)
+	profits.sort()
 	print('\033[1mwrought    \033[0;32mdaily profit/area\033[31m')
 	print('\033[30mrecipe                         \033[0mexpertise            \033[33mcapex \033[35mdaily opex \033[34mlogistics\033[0m')
 	for p in profits:
-		print(f'\033[53;1m{p.output:5} \033[53;32m{p.profit_per_area: 10,.0f} ', end='')
+		print(f'\033[53;1m{p.output:5} \033[53;32m{p.profit_per_day/p.area: 10,.0f} ', end='')
 		warnings = []
-		if p.low_volume:
+		if p.average_traded_7d < p.output_per_day * 20:
 			warnings.append('low volume')
 		if p.logistics_per_area > 1.5:
 			warnings.append('heavy logistics')
@@ -69,11 +69,13 @@ def calc_profit(recipe: Recipe, buildings: typing.Mapping[str, Building], materi
 	) * runs_per_day / building['AreaCost']
 	return Profit(output['Ticker'], recipe['RecipeName'],
 			expertise=building['Expertise'].replace('_', ' ').lower(),
-			profit_per_area=(profit_per_run * runs_per_day - worker_consumable_daily_cost) / building['AreaCost'],
+			profit_per_day=(profit_per_run * runs_per_day - worker_consumable_daily_cost),
+			area=building['AreaCost'],
 			capex=capex,
 			cost_per_day=cost_per_day,
 			logistics_per_area=logistics_per_area,
-			low_volume=output_price.average_traded < output_per_day * 20)
+			output_per_day=output_per_day,
+			average_traded_7d=output_price.average_traded_7d)
 
 def building_daily_cost(building: Building, prices: typing.Mapping[str, Price]) -> float:
 	consumption = {
@@ -131,27 +133,27 @@ class RawPrice(typing.TypedDict):
 @dataclasses.dataclass(eq=False, frozen=True, slots=True)
 class Price:
 	vwap: float
-	average_traded: float
+	average_traded_7d: float
 
-@dataclasses.dataclass(eq=False, slots=True)
+@dataclasses.dataclass(eq=False, frozen=True, slots=True)
 class Profit:
 	output: str
 	recipe: str
 	expertise: str
-	profit_per_area: float
+	profit_per_day: float
+	area: float
 	capex: float
 	cost_per_day: float
 	logistics_per_area: float
-	low_volume: bool
-	score: float = dataclasses.field(init=False)
-
-	def __post_init__(self) -> None:
-		self.score = self.profit_per_area
-		if self.low_volume:
-			self.score *= 0.2
+	output_per_day: float
+	average_traded_7d: float
 
 	def __lt__(self, other: Profit) -> bool:
-		return self.score < other.score
+		if (break_even := self.capex / self.profit_per_day) < 0:
+			break_even = 10000 - self.profit_per_day
+		if (other_break_even := other.capex / other.profit_per_day) < 0:
+			other_break_even = 10000 - other.profit_per_day
+		return break_even < other_break_even
 
 if __name__ == '__main__':
 	main()

+ 7 - 5
supply.py

@@ -64,8 +64,8 @@ def main() -> None:
 		buys: dict[str, dict[str, int]] = {}
 		iteration_weight = iteration_volume = 0
 		for planet in planets:
-			buy = planet.buy_for_target(target_days)
-			weight_used, volume_used = shipping_used(materials, buy)
+			buy = planet.supply_for_days(target_days)
+			weight_used, volume_used = shipping_used(materials, config.supply_config(planet.name).ignore_materials, buy)
 			iteration_weight += weight_used
 			iteration_volume += volume_used
 			if iteration_weight > args.weight or iteration_volume > args.volume:
@@ -148,9 +148,11 @@ def get_fio_burn(planet_names: typing.Sequence[str]) -> typing.Iterator[FIOBurn]
 		else:
 			raise ValueError(name + ' not found')
 
-def shipping_used(materials: dict[str, Material], buy: dict[str, int]) -> tuple[float, float]:
+def shipping_used(materials: dict[str, Material], ignore: typing.Collection[str], counts: dict[str, int]) -> tuple[float, float]:
 	weight = volume = 0
-	for ticker, amount in buy.items():
+	for ticker, amount in counts.items():
+		if ticker in ignore:
+			continue
 		weight += amount * materials[ticker]['Weight']
 		volume += amount * materials[ticker]['Volume']
 	return weight, volume
@@ -187,7 +189,7 @@ class Planet:
 			c['net_consumption'] = net
 			self.net_consumption.append(c)
 
-	def buy_for_target(self, target_days: float) -> dict[str, int]:
+	def supply_for_days(self, target_days: float) -> dict[str, int]:
 		buy: dict[str, int] = {}
 		for consumption in self.net_consumption:
 			ticker = consumption['MaterialTicker']

+ 60 - 12
ts/roi.ts

@@ -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
 }

+ 9 - 5
www/index.html

@@ -10,20 +10,24 @@
 </head>
 <body>
 	<main>
+        <label><input type="checkbox" id="low-volume">show low volume</label>
 		<table>
 			<thead>
 				<tr>
-					<th>wrought product</th>
+					<th>wrought<br>product</th>
 					<th>expertise</th>
-					<th>daily profit/area</th>
-					<th>capex</th>
-					<th>daily opex</th>
-					<th>logistics</th>
+					<th>daily<br>profit/area</th>
+					<th>break<br>even</th>
+					<th data-tooltip="construction cost of 1 building">capex</th>
+					<th data-tooltip="input and worker costs of 1 building (deterioration/repair not included)">daily<br>opex</th>
+					<th data-tooltip="max of input and output t and m³ per area">logistics</th>
+					<th data-tooltip="units output by 1 building over average daily traded">daily output<br>traded</th>
 				</tr>
 			</thead>
 			<tbody></tbody>
 		</table>
 	</main>
+	<div id="popover" popover="hint"></div>
 	<script src="roi.js"></script>
 </body>
 </html>

+ 24 - 2
www/style.css

@@ -23,6 +23,11 @@ a:hover {
 	color: #5ad;
 }
 
+input {
+	background-color: #111;
+	accent-color: #f70;
+}
+
 main {
 	width: 900px;
 	margin: 10px auto;
@@ -31,8 +36,11 @@ main {
 	box-shadow: 0 0 5px #222;
 
 	table {
-		td, th {
-			padding: 0.25em 0.5em;
+		width: 100%;
+		margin-top: 1em;
+		border-collapse: collapse;
+		th {
+			white-space: no-wrap;
 		}
 		tbody {
 			td:nth-child(1),
@@ -43,7 +51,21 @@ main {
 			td {
 				font-family: monospace;
 				text-align: right;
+				border-top: 1px solid #222;
 			}
 		}
 	}
 }
+
+*[data-tooltip] {
+	text-decoration: underline dashed;
+	cursor: help;
+}
+
+[popover="hint"] {
+	inset: unset;
+	color: inherit;
+	background-color: #222;
+	border: none;
+	opacity: 0.8;
+}