|
@@ -60,6 +60,7 @@ let roundTripOption: string = localStorage.getItem('roi-round-trip') || 'omit';
|
|
|
let showNegativeProfit: boolean = localStorage.getItem('roi-show-negative') !== 'false';
|
|
let showNegativeProfit: boolean = localStorage.getItem('roi-show-negative') !== 'false';
|
|
|
let workingCapitalOption: string = localStorage.getItem('roi-working-capital-opt') || 'dynamic';
|
|
let workingCapitalOption: string = localStorage.getItem('roi-working-capital-opt') || 'dynamic';
|
|
|
let targetPermitOption: string = localStorage.getItem('roi-target-permit') || '2';
|
|
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() {
|
|
async function render() {
|
|
|
const tbody = document.querySelector('tbody')!;
|
|
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('')}
|
|
${Array.from({length: 49}, (_, i) => `<option value="${i + 2}">${i + 2}</option>`).join('')}
|
|
|
</select>
|
|
</select>
|
|
|
</label>
|
|
</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');
|
|
const table = document.querySelector('table');
|
|
|
if (table) table.parentNode?.insertBefore(controls, 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('round-trip') as HTMLSelectElement).value = roundTripOption;
|
|
|
(document.getElementById('working-capital') as HTMLSelectElement).value = workingCapitalOption;
|
|
(document.getElementById('working-capital') as HTMLSelectElement).value = workingCapitalOption;
|
|
|
(document.getElementById('target-permit') as HTMLSelectElement).value = targetPermitOption;
|
|
(document.getElementById('target-permit') as HTMLSelectElement).value = targetPermitOption;
|
|
|
|
|
+ (document.getElementById('percentile-mode') as HTMLSelectElement).value = percentileMode;
|
|
|
|
|
|
|
|
document.getElementById('capex-metric')!.addEventListener('change', (e) => {
|
|
document.getElementById('capex-metric')!.addEventListener('change', (e) => {
|
|
|
capexMetric = (e.target as HTMLSelectElement).value as MetricType;
|
|
capexMetric = (e.target as HTMLSelectElement).value as MetricType;
|
|
@@ -156,6 +165,10 @@ async function render() {
|
|
|
targetPermitOption = (e.target as HTMLSelectElement).value;
|
|
targetPermitOption = (e.target as HTMLSelectElement).value;
|
|
|
render();
|
|
render();
|
|
|
});
|
|
});
|
|
|
|
|
+ document.getElementById('percentile-mode')!.addEventListener('change', (e) => {
|
|
|
|
|
+ percentileMode = (e.target as HTMLSelectElement).value as 'relative' | 'absolute';
|
|
|
|
|
+ render();
|
|
|
|
|
+ });
|
|
|
metricControlsInitialized = true;
|
|
metricControlsInitialized = true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -166,7 +179,7 @@ async function render() {
|
|
|
'capex_val', 'opex_val', 'normalized_logistics_per_base', 'market_capacity_base'
|
|
'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) => {
|
|
ths.forEach((th, i) => {
|
|
|
if (keys[i]) {
|
|
if (keys[i]) {
|
|
@@ -333,24 +346,24 @@ async function render() {
|
|
|
for (const p of profitsWithMetrics) {
|
|
for (const p of profitsWithMetrics) {
|
|
|
const tr = document.createElement('tr');
|
|
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 = `
|
|
tr.innerHTML = `
|
|
|
<td>${p.outputs.map(o => o.ticker).join(', ')}</td>
|
|
<td>${p.outputs.map(o => o.ticker).join(', ')}</td>
|
|
|
<td>${expertise[p.expertise]}</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')!;
|
|
const output = tr.querySelector('td')!;
|
|
@@ -449,6 +462,7 @@ function saveState() {
|
|
|
localStorage.setItem('roi-round-trip', roundTripOption);
|
|
localStorage.setItem('roi-round-trip', roundTripOption);
|
|
|
localStorage.setItem('roi-working-capital-opt', workingCapitalOption);
|
|
localStorage.setItem('roi-working-capital-opt', workingCapitalOption);
|
|
|
localStorage.setItem('roi-target-permit', targetPermitOption);
|
|
localStorage.setItem('roi-target-permit', targetPermitOption);
|
|
|
|
|
+ localStorage.setItem('roi-pct-mode', percentileMode);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function color(n: number, low: number, high: number): string {
|
|
function color(n: number, low: number, high: number): string {
|