|
@@ -35,8 +35,6 @@ 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: These state variables track the current interactive table sort configuration.
|
|
|
|
|
-// By defaulting to 'break_even' and 'true' (ascending), the initial page load mirrors the old default behavior.
|
|
|
|
|
let currentSortKey: keyof Profit | 'outputs' = 'break_even';
|
|
let currentSortKey: keyof Profit | 'outputs' = 'break_even';
|
|
|
let currentSortAsc: boolean = true;
|
|
let currentSortAsc: boolean = true;
|
|
|
let headersInitialized = false;
|
|
let headersInitialized = false;
|
|
@@ -50,13 +48,14 @@ async function render() {
|
|
|
roiCache[cx] = getROI(cx);
|
|
roiCache[cx] = getROI(cx);
|
|
|
const {lastModified, profits} = await roiCache[cx];
|
|
const {lastModified, profits} = await roiCache[cx];
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: On the very first render, we grab all table headers and wire them up with click listeners.
|
|
|
|
|
- // The `keys` array maps 1:1 with the columns defined in the HTML structure.
|
|
|
|
|
if (!headersInitialized) {
|
|
if (!headersInitialized) {
|
|
|
const ths = document.querySelectorAll('th');
|
|
const ths = document.querySelectorAll('th');
|
|
|
|
|
+ // EXTREME DETAIL: We swapped 'output_per_day' for 'market_capacity_area'.
|
|
|
|
|
+ // Because this array index directly maps to the DOM headers, clicking the 8th
|
|
|
|
|
+ // table header now natively routes to the new metric for dynamic sorting.
|
|
|
const keys: (keyof Profit | 'outputs')[] = [
|
|
const keys: (keyof Profit | 'outputs')[] = [
|
|
|
'outputs', 'expertise', 'profit_per_area', 'break_even',
|
|
'outputs', 'expertise', 'profit_per_area', 'break_even',
|
|
|
- 'capex', 'cost_per_day', 'logistics_per_area', 'output_per_day'
|
|
|
|
|
|
|
+ 'capex', 'cost_per_day', 'logistics_per_area', 'market_capacity_area'
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
ths.forEach((th, i) => {
|
|
ths.forEach((th, i) => {
|
|
@@ -65,15 +64,11 @@ async function render() {
|
|
|
th.title = 'Click to sort';
|
|
th.title = 'Click to sort';
|
|
|
th.addEventListener('click', () => {
|
|
th.addEventListener('click', () => {
|
|
|
if (currentSortKey === keys[i]) {
|
|
if (currentSortKey === keys[i]) {
|
|
|
- // Flip the sort direction if clicking the same column twice
|
|
|
|
|
currentSortAsc = !currentSortAsc;
|
|
currentSortAsc = !currentSortAsc;
|
|
|
} else {
|
|
} else {
|
|
|
- // Switch to a new column
|
|
|
|
|
currentSortKey = keys[i];
|
|
currentSortKey = keys[i];
|
|
|
- // Break Even defaults to Lowest-to-Highest. All other numbers default to Highest-to-Lowest.
|
|
|
|
|
currentSortAsc = keys[i] === 'break_even' ? true : false;
|
|
currentSortAsc = keys[i] === 'break_even' ? true : false;
|
|
|
}
|
|
}
|
|
|
- // Re-trigger the render loop with the new sorting state
|
|
|
|
|
render();
|
|
render();
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -102,8 +97,9 @@ async function render() {
|
|
|
if (!buildingFound)
|
|
if (!buildingFound)
|
|
|
selectedBuilding = '';
|
|
selectedBuilding = '';
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: We extract the filtering logic into an explicit filter pass prior to rendering.
|
|
|
|
|
- // This separates data culling from the actual DOM string-building loop.
|
|
|
|
|
|
|
+ // EXTREME DETAIL: Even though we removed 'output_per_day' and 'average_traded_7d' from
|
|
|
|
|
+ // the visual table, they are still present in the JSON backend structure.
|
|
|
|
|
+ // This means our 'lowVolume' filter still works perfectly without requiring any math changes!
|
|
|
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;
|
|
|
if (!lowVolume.checked && volumeRatio > 0.05) return false;
|
|
if (!lowVolume.checked && volumeRatio > 0.05) return false;
|
|
@@ -112,8 +108,6 @@ async function render() {
|
|
|
return true;
|
|
return true;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: We execute the interactive sorting logic based on the user's header click state.
|
|
|
|
|
- // 'outputs' requires special handling because it is an Array of MatPrice objects, not a primitive string/number.
|
|
|
|
|
filteredProfits.sort((a, b) => {
|
|
filteredProfits.sort((a, b) => {
|
|
|
let valA: any = a[currentSortKey as keyof Profit];
|
|
let valA: any = a[currentSortKey as keyof Profit];
|
|
|
let valB: any = b[currentSortKey as keyof Profit];
|
|
let valB: any = b[currentSortKey as keyof Profit];
|
|
@@ -129,11 +123,11 @@ async function render() {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
for (const p of filteredProfits) {
|
|
for (const p of filteredProfits) {
|
|
|
- const volumeRatio = p.output_per_day / p.average_traded_7d;
|
|
|
|
|
const tr = document.createElement('tr');
|
|
const tr = document.createElement('tr');
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: We no longer recalculate profitPerArea and breakEven here.
|
|
|
|
|
- // We simply read the properties established by the Python backend via the JSON contract.
|
|
|
|
|
|
|
+ // EXTREME DETAIL: We swapped the dual-line output code for a single <td> rendering the new
|
|
|
|
|
+ // market_capacity_area metric. The color mapping scale ranges from 20 (Red) to 500 (Cyan).
|
|
|
|
|
+ // For reference: a capacity of 20 means building 1 area captures exactly 5% of the market volume.
|
|
|
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>
|
|
@@ -142,10 +136,7 @@ async function render() {
|
|
|
<td style="color: ${color(p.capex, 300_000, 40_000)}">${formatWhole(p.capex)}</td>
|
|
<td style="color: ${color(p.capex, 300_000, 40_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.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 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>
|
|
|
|
|
|
|
+ <td style="color: ${color(p.market_capacity_area, 20, 500)}">${formatWhole(p.market_capacity_area)}</td>
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
const output = tr.querySelector('td')!;
|
|
const output = tr.querySelector('td')!;
|
|
@@ -161,6 +152,11 @@ async function render() {
|
|
|
`- ${formatWhole(p.worker_consumable_cost_per_day)} = ${formatDecimal(p.profit_per_day)}\n` +
|
|
`- ${formatWhole(p.worker_consumable_cost_per_day)} = ${formatDecimal(p.profit_per_day)}\n` +
|
|
|
`${formatDecimal(p.profit_per_day)} / ${formatWhole(p.area)} area = ${formatDecimal(p.profit_per_area)}`;
|
|
`${formatDecimal(p.profit_per_day)} / ${formatWhole(p.area)} area = ${formatDecimal(p.profit_per_area)}`;
|
|
|
|
|
|
|
|
|
|
+ // EXTREME DETAIL: Because we are condensing two variables into a single number, providing a tooltip
|
|
|
|
|
+ // explaining exactly how the math breaks down is critical for the end-user experience.
|
|
|
|
|
+ const marketCell = tr.querySelectorAll('td')[7];
|
|
|
|
|
+ marketCell.dataset.tooltip = `Market Capacity: ${formatWhole(p.average_traded_7d)} traded/day ÷ ${formatDecimal(p.output_per_day / p.area)} produced/day/area = ${formatWhole(p.market_capacity_area)} equivalent areas`;
|
|
|
|
|
+
|
|
|
tbody.appendChild(tr);
|
|
tbody.appendChild(tr);
|
|
|
}
|
|
}
|
|
|
document.getElementById('last-updated')!.textContent =
|
|
document.getElementById('last-updated')!.textContent =
|
|
@@ -168,7 +164,6 @@ async function render() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function color(n: number, low: number, high: number): string {
|
|
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);
|
|
const scale = Math.min(Math.max((n - low) / (high - low), 0), 1);
|
|
|
return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`;
|
|
return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`;
|
|
|
}
|
|
}
|
|
@@ -200,8 +195,9 @@ interface Profit {
|
|
|
logistics_per_area: number
|
|
logistics_per_area: number
|
|
|
output_per_day: number
|
|
output_per_day: number
|
|
|
average_traded_7d: number
|
|
average_traded_7d: number
|
|
|
- profit_per_area: number // Added pre-calculated property tracking
|
|
|
|
|
- break_even: number // Added pre-calculated property tracking
|
|
|
|
|
|
|
+ profit_per_area: number
|
|
|
|
|
+ break_even: number
|
|
|
|
|
+ market_capacity_area: number // Added pre-calculated property tracking
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface MatPrice {
|
|
interface MatPrice {
|