|
@@ -8,7 +8,6 @@ async function getROI(cx: string) {
|
|
|
return {lastModified, profits};
|
|
return {lastModified, profits};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// EXTREME DETAIL: Hoisted the MetricType so it can be utilized by the state initialization block below.
|
|
|
|
|
type MetricType = 'vwap' | 'bid' | 'ask';
|
|
type MetricType = 'vwap' | 'bid' | 'ask';
|
|
|
|
|
|
|
|
const lowVolume = document.querySelector('input#low-volume') as HTMLInputElement;
|
|
const lowVolume = document.querySelector('input#low-volume') as HTMLInputElement;
|
|
@@ -38,25 +37,20 @@ const formatDecimal = new Intl.NumberFormat(undefined,
|
|
|
{maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: 'lessPrecision'}).format;
|
|
{maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: 'lessPrecision'}).format;
|
|
|
const formatWhole = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format;
|
|
const formatWhole = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format;
|
|
|
|
|
|
|
|
-// EXTREME DETAIL: --- STATE INITIALIZATION ---
|
|
|
|
|
-// Upon script execution (page load), we immediately query the browser's localStorage.
|
|
|
|
|
-// If valid keys exist, we override the default DOM selections with the persisted state.
|
|
|
|
|
if (localStorage.getItem('roi-cx')) cxSelect.value = localStorage.getItem('roi-cx')!;
|
|
if (localStorage.getItem('roi-cx')) cxSelect.value = localStorage.getItem('roi-cx')!;
|
|
|
if (localStorage.getItem('roi-expertise')) expertiseSelect.value = localStorage.getItem('roi-expertise')!;
|
|
if (localStorage.getItem('roi-expertise')) expertiseSelect.value = localStorage.getItem('roi-expertise')!;
|
|
|
if (localStorage.getItem('roi-low-volume')) lowVolume.checked = localStorage.getItem('roi-low-volume') === 'true';
|
|
if (localStorage.getItem('roi-low-volume')) lowVolume.checked = localStorage.getItem('roi-low-volume') === 'true';
|
|
|
|
|
|
|
|
-// The building options haven't been dynamically generated yet, so we store the target in a temporary variable.
|
|
|
|
|
let savedBuilding = localStorage.getItem('roi-building') || '';
|
|
let savedBuilding = localStorage.getItem('roi-building') || '';
|
|
|
|
|
|
|
|
let currentSortKey: keyof ProfitWithMetrics | 'outputs' = (localStorage.getItem('roi-sort-key') as any) || 'break_even';
|
|
let currentSortKey: keyof ProfitWithMetrics | 'outputs' = (localStorage.getItem('roi-sort-key') as any) || 'break_even';
|
|
|
-let currentSortAsc: boolean = localStorage.getItem('roi-sort-asc') !== 'false'; // Defaults to true if null
|
|
|
|
|
|
|
+let currentSortAsc: boolean = localStorage.getItem('roi-sort-asc') !== 'false';
|
|
|
let headersInitialized = false;
|
|
let headersInitialized = false;
|
|
|
let metricControlsInitialized = false;
|
|
let metricControlsInitialized = false;
|
|
|
|
|
|
|
|
let capexMetric: MetricType = (localStorage.getItem('roi-capex-metric') as MetricType) || 'vwap';
|
|
let capexMetric: MetricType = (localStorage.getItem('roi-capex-metric') as MetricType) || 'vwap';
|
|
|
let opexMetric: MetricType = (localStorage.getItem('roi-opex-metric') as MetricType) || 'vwap';
|
|
let opexMetric: MetricType = (localStorage.getItem('roi-opex-metric') as MetricType) || 'vwap';
|
|
|
let revenueMetric: MetricType = (localStorage.getItem('roi-revenue-metric') as MetricType) || 'vwap';
|
|
let revenueMetric: MetricType = (localStorage.getItem('roi-revenue-metric') as MetricType) || 'vwap';
|
|
|
-// ------------------------------------------
|
|
|
|
|
|
|
|
|
|
async function render() {
|
|
async function render() {
|
|
|
const tbody = document.querySelector('tbody')!;
|
|
const tbody = document.querySelector('tbody')!;
|
|
@@ -84,7 +78,6 @@ async function render() {
|
|
|
const table = document.querySelector('table');
|
|
const table = document.querySelector('table');
|
|
|
if (table) table.parentNode?.insertBefore(controls, table);
|
|
if (table) table.parentNode?.insertBefore(controls, table);
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: Apply the stored state defaults to the newly generated dropdown nodes.
|
|
|
|
|
(document.getElementById('capex-metric') as HTMLSelectElement).value = capexMetric;
|
|
(document.getElementById('capex-metric') as HTMLSelectElement).value = capexMetric;
|
|
|
(document.getElementById('opex-metric') as HTMLSelectElement).value = opexMetric;
|
|
(document.getElementById('opex-metric') as HTMLSelectElement).value = opexMetric;
|
|
|
(document.getElementById('revenue-metric') as HTMLSelectElement).value = revenueMetric;
|
|
(document.getElementById('revenue-metric') as HTMLSelectElement).value = revenueMetric;
|
|
@@ -114,7 +107,20 @@ async function render() {
|
|
|
ths.forEach((th, i) => {
|
|
ths.forEach((th, i) => {
|
|
|
if (keys[i]) {
|
|
if (keys[i]) {
|
|
|
th.style.cursor = 'pointer';
|
|
th.style.cursor = 'pointer';
|
|
|
- th.title = 'Click to sort';
|
|
|
|
|
|
|
+ th.title = ''; // Remove native title to prevent double-tooltips
|
|
|
|
|
+
|
|
|
|
|
+ // EXTREME DETAIL: We inject dynamic string content and custom tooltips directly into the DOM headers.
|
|
|
|
|
+ // By routing these explanations into `dataset.tooltip`, the existing popover engine picks them up
|
|
|
|
|
+ // automatically, rendering a clean, stylized popup on hover.
|
|
|
|
|
+ if (keys[i] === 'market_capacity_area') {
|
|
|
|
|
+ th.textContent = 'Market Cap (Areas)';
|
|
|
|
|
+ th.dataset.tooltip = 'Click to sort.\n\nMarket Capacity: 7-day average traded volume ÷ daily output per area. Indicates how many areas you can build before saturating the market.';
|
|
|
|
|
+ } else if (keys[i] === 'break_even') {
|
|
|
|
|
+ th.dataset.tooltip = 'Click to sort.\n\nBreak Even: (CapEx + 3 days of OpEx) ÷ daily profit. Includes 3 days of operating costs as working capital.';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ th.dataset.tooltip = 'Click to sort.';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
th.addEventListener('click', () => {
|
|
th.addEventListener('click', () => {
|
|
|
if (currentSortKey === keys[i]) {
|
|
if (currentSortKey === keys[i]) {
|
|
|
currentSortAsc = !currentSortAsc;
|
|
currentSortAsc = !currentSortAsc;
|
|
@@ -133,8 +139,7 @@ async function render() {
|
|
|
const buildings: {ticker: string, expertise: keyof typeof expertise}[] = Array.from(buildingTickers)
|
|
const buildings: {ticker: string, expertise: keyof typeof expertise}[] = Array.from(buildingTickers)
|
|
|
.map((building) => ({ticker: building, expertise: profits.find(p => p.building === building)!.expertise}))
|
|
.map((building) => ({ticker: building, expertise: profits.find(p => p.building === building)!.expertise}))
|
|
|
.sort((a, b) => a.ticker.localeCompare(b.ticker));
|
|
.sort((a, b) => a.ticker.localeCompare(b.ticker));
|
|
|
-
|
|
|
|
|
- // EXTREME DETAIL: Inject the stored 'savedBuilding' target if this is the first execution.
|
|
|
|
|
|
|
+
|
|
|
let selectedBuilding = buildingSelect.value || savedBuilding;
|
|
let selectedBuilding = buildingSelect.value || savedBuilding;
|
|
|
let buildingFound = false;
|
|
let buildingFound = false;
|
|
|
buildingSelect.innerHTML = '<option value="">(all)</option>';
|
|
buildingSelect.innerHTML = '<option value="">(all)</option>';
|
|
@@ -151,9 +156,8 @@ async function render() {
|
|
|
}
|
|
}
|
|
|
if (!buildingFound)
|
|
if (!buildingFound)
|
|
|
selectedBuilding = '';
|
|
selectedBuilding = '';
|
|
|
-
|
|
|
|
|
- // Clear the injection buffer so future renders correctly rely purely on DOM/User manipulation.
|
|
|
|
|
- savedBuilding = '';
|
|
|
|
|
|
|
+
|
|
|
|
|
+ savedBuilding = '';
|
|
|
|
|
|
|
|
const filteredProfits = profits.filter(p => {
|
|
const filteredProfits = profits.filter(p => {
|
|
|
const volumeRatio = p.output_per_day / p.average_traded_7d;
|
|
const volumeRatio = p.output_per_day / p.average_traded_7d;
|
|
@@ -190,7 +194,6 @@ async function render() {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
for (const p of profitsWithMetrics) {
|
|
for (const p of profitsWithMetrics) {
|
|
|
- const volumeRatio = p.output_per_day / p.average_traded_7d;
|
|
|
|
|
const tr = document.createElement('tr');
|
|
const tr = document.createElement('tr');
|
|
|
|
|
|
|
|
tr.innerHTML = `
|
|
tr.innerHTML = `
|
|
@@ -221,10 +224,7 @@ async function render() {
|
|
|
document.getElementById('last-updated')!.textContent =
|
|
document.getElementById('last-updated')!.textContent =
|
|
|
`last updated: ${lastModified.toLocaleString(undefined, {dateStyle: 'full', timeStyle: 'long', hour12: false})}`;
|
|
`last updated: ${lastModified.toLocaleString(undefined, {dateStyle: 'full', timeStyle: 'long', hour12: false})}`;
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: By calling saveState() at the very conclusion of the render loop,
|
|
|
|
|
- // we take a final snapshot of the fully sanitized UI state (ensuring we don't accidentally
|
|
|
|
|
- // save invalid building + expertise configurations to the hard drive).
|
|
|
|
|
- saveState();
|
|
|
|
|
|
|
+ saveState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function saveState() {
|
|
function saveState() {
|