Prechádzať zdrojové kódy

Add percentile display toggle

Purpose: Simplify and declutter the table UI by changing the dual-percentile output string `(rel / abs)` into a single output string toggled by the user.
        - Added `percentileMode` state tied to localStorage (`roi-pct-mode`).
        - Injected a new select dropdown into the metrics controls to swap between "Normalized (Weighted)" and "Absolute".
        - Updated the `getPercentiles` lookup in the render loop to fetch `.abs` or `.rel` dynamically based on state.
        - Removed the parentheses from the HTML template literal outputs.
        - Updated the tooltip explainer to document the new behavior.
Thomas Knott 1 týždeň pred
rodič
commit
f9df56d946
1 zmenil súbory, kde vykonal 29 pridanie a 15 odobranie
  1. 29 15
      ts/roi.ts

+ 29 - 15
ts/roi.ts

@@ -60,6 +60,7 @@ let roundTripOption: string = localStorage.getItem('roi-round-trip') || 'omit';
 let showNegativeProfit: boolean = localStorage.getItem('roi-show-negative') !== 'false';
 let workingCapitalOption: string = localStorage.getItem('roi-working-capital-opt') || 'dynamic';
 let targetPermitOption: string = localStorage.getItem('roi-target-permit') || '2';
+let percentileMode: 'relative' | 'absolute' = (localStorage.getItem('roi-pct-mode') as 'relative' | 'absolute') || 'relative';
 
 async function render() {
 	const tbody = document.querySelector('tbody')!;
@@ -116,6 +117,13 @@ async function render() {
 					${Array.from({length: 49}, (_, i) => `<option value="${i + 2}">${i + 2}</option>`).join('')}
 				</select>
 			</label>
+			<label style="margin-left: 15px;">
+				Percentiles: 
+				<select id="percentile-mode">
+					<option value="relative">Normalized (Weighted)</option>
+					<option value="absolute">Absolute</option>
+				</select>
+			</label>
 		`;
 		const table = document.querySelector('table');
 		if (table) table.parentNode?.insertBefore(controls, table);
@@ -127,6 +135,7 @@ async function render() {
 		(document.getElementById('round-trip') as HTMLSelectElement).value = roundTripOption;
 		(document.getElementById('working-capital') as HTMLSelectElement).value = workingCapitalOption;
 		(document.getElementById('target-permit') as HTMLSelectElement).value = targetPermitOption;
+		(document.getElementById('percentile-mode') as HTMLSelectElement).value = percentileMode;
 
 		document.getElementById('capex-metric')!.addEventListener('change', (e) => {
 			capexMetric = (e.target as HTMLSelectElement).value as MetricType;
@@ -156,6 +165,10 @@ async function render() {
 			targetPermitOption = (e.target as HTMLSelectElement).value;
 			render();
 		});
+		document.getElementById('percentile-mode')!.addEventListener('change', (e) => {
+			percentileMode = (e.target as HTMLSelectElement).value as 'relative' | 'absolute';
+			render();
+		});
 		metricControlsInitialized = true;
 	}
 
@@ -166,7 +179,7 @@ async function render() {
 			'capex_val', 'opex_val', 'normalized_logistics_per_base', 'market_capacity_base'
 		];
 		
-		const pctExplainer = '\n(Percentiles: Relative Volume / Absolute. 100.0% is the most desirable outcome. Relative rank is weighted by total market cash flow.)';
+		const pctExplainer = '\n(Percentiles: 100.0% is the most desirable outcome. Toggle between Absolute and Normalized (weighted by market cash flow) using the controls.)';
 
 		ths.forEach((th, i) => {
 			if (keys[i]) {
@@ -333,24 +346,24 @@ async function render() {
 	for (const p of profitsWithMetrics) {
 		const tr = document.createElement('tr');
 		
-		// Map the raw value to both absolute and relative percentile ranks.
-		const pctProfit = getPercentiles(p.profit_per_base, arrProfit, false);
-		const pctBreak = getPercentiles(p.break_even, arrBreak, true);
-		const pctCapex = getPercentiles(p.capex_val, arrCapex, true);
-		const pctOpex = getPercentiles(p.opex_val, arrOpex, true);
-		const pctLog = getPercentiles(p.normalized_logistics_per_base, arrLog, true);
-		const pctCap = getPercentiles(p.market_capacity_base, arrCap, false);
+		// Map the raw value to the selected percentile rank.
+		const pctProfit = getPercentiles(p.profit_per_base, arrProfit, false)[percentileMode === 'absolute' ? 'abs' : 'rel'];
+		const pctBreak = getPercentiles(p.break_even, arrBreak, true)[percentileMode === 'absolute' ? 'abs' : 'rel'];
+		const pctCapex = getPercentiles(p.capex_val, arrCapex, true)[percentileMode === 'absolute' ? 'abs' : 'rel'];
+		const pctOpex = getPercentiles(p.opex_val, arrOpex, true)[percentileMode === 'absolute' ? 'abs' : 'rel'];
+		const pctLog = getPercentiles(p.normalized_logistics_per_base, arrLog, true)[percentileMode === 'absolute' ? 'abs' : 'rel'];
+		const pctCap = getPercentiles(p.market_capacity_base, arrCap, false)[percentileMode === 'absolute' ? 'abs' : 'rel'];
 		
-		// Interplate the Rel/Abs format string requested directly into the <small> span wrapper.
+		// Interplate the format string requested directly into the <small> span wrapper.
 		tr.innerHTML = `
 			<td>${p.outputs.map(o => o.ticker).join(', ')}</td>
 			<td>${expertise[p.expertise]}</td>
-			<td style="color: ${color(p.profit_per_base, 0, 150000)}">${formatSigFig(p.profit_per_base)} <span style="font-size: 0.85em; opacity: 0.6;">(${pctProfit.rel} / ${pctProfit.abs})</span></td>
-			<td><span style="color: ${color(p.break_even, 30, 3)}">${formatSigFig(p.break_even)}</span>d <span style="font-size: 0.85em; opacity: 0.6;">(${pctBreak.rel} / ${pctBreak.abs})</span></td>
-			<td style="color: ${color(p.capex_val, 3_000_000, 400_000)}">${formatSigFig(p.capex_val)} <span style="font-size: 0.85em; opacity: 0.6;">(${pctCapex.rel} / ${pctCapex.abs})</span></td>
-			<td style="color: ${color(p.opex_val, 400_000, 10_000)}">${formatSigFig(p.opex_val)} <span style="font-size: 0.85em; opacity: 0.6;">(${pctOpex.rel} / ${pctOpex.abs})</span></td>
-			<td style="color: ${color(p.normalized_logistics_per_base, 1.0, 0.1)}">${formatSigFig(p.logistics_per_base)} ${p.logistics_bottleneck} <span style="font-size: 0.85em; opacity: 0.6;">(${pctLog.rel} / ${pctLog.abs})</span></td>
-			<td style="color: ${color(p.market_capacity_base, 0.04, 1)}">${formatSigFig(p.market_capacity_base)} <span style="font-size: 0.85em; opacity: 0.6;">(${pctCap.rel} / ${pctCap.abs})</span></td>
+			<td style="color: ${color(p.profit_per_base, 0, 150000)}">${formatSigFig(p.profit_per_base)} <span style="font-size: 0.85em; opacity: 0.6;">${pctProfit}</span></td>
+			<td><span style="color: ${color(p.break_even, 30, 3)}">${formatSigFig(p.break_even)}</span>d <span style="font-size: 0.85em; opacity: 0.6;">${pctBreak}</span></td>
+			<td style="color: ${color(p.capex_val, 3_000_000, 400_000)}">${formatSigFig(p.capex_val)} <span style="font-size: 0.85em; opacity: 0.6;">${pctCapex}</span></td>
+			<td style="color: ${color(p.opex_val, 400_000, 10_000)}">${formatSigFig(p.opex_val)} <span style="font-size: 0.85em; opacity: 0.6;">${pctOpex}</span></td>
+			<td style="color: ${color(p.normalized_logistics_per_base, 1.0, 0.1)}">${formatSigFig(p.logistics_per_base)} ${p.logistics_bottleneck} <span style="font-size: 0.85em; opacity: 0.6;">${pctLog}</span></td>
+			<td style="color: ${color(p.market_capacity_base, 0.04, 1)}">${formatSigFig(p.market_capacity_base)} <span style="font-size: 0.85em; opacity: 0.6;">${pctCap}</span></td>
 		`;
 
 		const output = tr.querySelector('td')!;
@@ -449,6 +462,7 @@ function saveState() {
 	localStorage.setItem('roi-round-trip', roundTripOption);
 	localStorage.setItem('roi-working-capital-opt', workingCapitalOption);
 	localStorage.setItem('roi-target-permit', targetPermitOption);
+	localStorage.setItem('roi-pct-mode', percentileMode);
 }
 
 function color(n: number, low: number, high: number): string {