Spaces:
Running
Running
| const TIER_LABEL = { | |
| tier1: "Point · point accuracy", | |
| tier2: "Ranking · cross-sectional IC", | |
| tier3: "Decision · portfolio backtest" | |
| }; | |
| const TIER_SHORT = { tier1: "Point", tier2: "Ranking", tier3: "Decision" }; | |
| const ASPECT = { tier1: "pointwise", tier2: "ic", tier3: "portfolio" }; | |
| const METRIC_BASE_ORDER = ["hr", "hrp", "hrpp", "mase", "mase_diff", "ic", "return", "sharpe", "mdd", "vol"]; | |
| const METRIC_INFO = { | |
| hr: ["Hit Rate", "Share of cases where the predicted direction matches the realized direction."], | |
| hrp: ["Hit Rate (Delta)", "Share of cases where both the level direction and first-difference direction are correct."], | |
| hrpp: ["Hit Rate (Delta2)", "Share of cases where the level, first-difference, and second-difference directions are all correct."], | |
| mase: ["MASE (relative)", "Normalized absolute error between predicted and realized percentage changes."], | |
| mase_diff: ["MASE (diff)", "Absolute forecast error normalized by the period-difference scale."], | |
| ic: ["Information Coefficient", "Cross-sectional rank correlation between predicted and realized percentage changes."], | |
| return: ["Return", "Annualized return of an equal-weight long portfolio built from top predicted assets."], | |
| sharpe: ["Sharpe", "Annualized Sharpe ratio of the strategy returns."], | |
| mdd: ["Max Drawdown", "Maximum peak-to-trough decline of the NAV path. Values closer to 0 are better."], | |
| vol: ["Volatility", "Annualized standard deviation of the strategy returns."] | |
| }; | |
| const state = { | |
| data: null, | |
| records: [], | |
| models: [], | |
| modelConfigs: new Map(), | |
| excludedModelTypes: new Set(), | |
| excludedMetrics: new Set(), | |
| excludedTypes: new Set(), | |
| excludedTargets: new Set(), | |
| excludedDetails: new Set(), | |
| excludedFrequencies: new Set(), | |
| collapsedFilters: new Set(), | |
| collapsedSubcards: new Set(), | |
| recordsReady: false, | |
| view: "overview", | |
| showDetailRank: true, | |
| showModelInfo: true, | |
| sort: { key: "overall", asc: true }, | |
| metricsSort: { key: "rank", asc: true }, | |
| metric: { tier: "tier1", key: null, detail: "__all__", frequency: "__all__", sort: { key: "rank", asc: true } } | |
| }; | |
| const $ = (id) => document.getElementById(id); | |
| const isExaone = (model) => model === "exaone-forecast" || model.startsWith("exaone-"); | |
| const displayName = (model) => isExaone(model) ? "EXAONE Forecast Finance" : model; | |
| const modelConfig = (model) => state.modelConfigs.get(model) || { model, model_type: "unknown", org: "Unknown" }; | |
| const modelType = (model) => modelConfig(model).model_type || "unknown"; | |
| const modelOrg = (model) => { | |
| const org = modelConfig(model).org || ""; | |
| return org.toLowerCase() === "unknown" ? "" : org; | |
| }; | |
| const modelLink = (model) => modelConfig(model).model_link || ""; | |
| const modelSize = (model) => modelConfig(model).model_size || ""; | |
| const modelSubmitTime = (model) => modelConfig(model).submit_time || ""; | |
| const orgColors = { | |
| Alibaba: "#0891b2", | |
| Amazon: "#2563eb", | |
| Datadog: "#16a34a", | |
| "Google Research": "#9333ea", | |
| "IBM Research": "#f97316", | |
| "LG AI Research": "#e11d48", | |
| PriorLabs: "#be185d", | |
| Salesforce: "#0f766e", | |
| ServiceNow: "#7c3aed", | |
| "Tsinghua University": "#a16207", | |
| Unspecified: "#64748b", | |
| "Zhejiang University": "#dc2626" | |
| }; | |
| const fallbackOrgPalette = ["#1d4ed8", "#15803d", "#c026d3", "#0e7490", "#b45309", "#4338ca"]; | |
| const organizationName = (model) => modelOrg(model) || "Unspecified"; | |
| const organizationColor = (org) => orgColors[org] || fallbackOrgPalette[Math.abs(String(org).split("").reduce((sum, char) => sum + char.charCodeAt(0), 0)) % fallbackOrgPalette.length]; | |
| const modelTypeLabel = (type) => { | |
| const labels = { | |
| "zero-shot": "Pretrained", | |
| pretrained: "Pretrained", | |
| statistical: "Statistical", | |
| unknown: "" | |
| }; | |
| return labels[type] || type; | |
| }; | |
| const includedModels = () => state.models.filter((model) => !state.excludedModelTypes.has(modelType(model))); | |
| const displayDetailLabel = (label) => { | |
| const map = { | |
| "Crypto Pair Close (Top 100)": "Crypto Pair Close", | |
| "OHLCV Close": "Close" | |
| }; | |
| return map[label] || label; | |
| }; | |
| const renderModelCell = (model) => { | |
| const link = modelLink(model); | |
| const label = displayName(model); | |
| if (!link) return `<td class="model-name">${label}</td>`; | |
| return `<td class="model-name model-link"><a href="${link}" target="_blank" rel="noopener noreferrer">${label}</a></td>`; | |
| }; | |
| const renderModelTypeCell = (model) => { | |
| const type = modelType(model); | |
| const label = modelTypeLabel(type); | |
| if (!label) return '<td class="model-type-cell"></td>'; | |
| return `<td class="model-type-cell"><span class="model-type-mark type-${type}" title="${label}" aria-label="${label}"></span></td>`; | |
| }; | |
| const renderModelTypeFilterLabel = (type, label) => { | |
| if (!label) return ""; | |
| return `<span class="model-type-mark type-${type}" aria-hidden="true"></span><span>${label}</span>`; | |
| }; | |
| const DETAIL_CATEGORY_EXPLANATIONS = { | |
| "Bond Total Return Index": "A total return bond index that reflects both price changes and interest income. It evaluates the return path experienced by bond investors rather than the level of yields alone.", | |
| Breakeven: "A market-based inflation expectation measure derived from the spread between nominal government bond yields and inflation-linked bond yields.", | |
| "Corporate Yield": "The yield level required in corporate bond markets, reflecting both corporate funding costs and market pricing of credit risk.", | |
| "Credit Spread": "The spread between corporate or credit bond yields and safe asset yields. It tends to widen when default risk, liquidity stress, or recession risk rises.", | |
| "Forward Inflation": "A forward-looking market measure of expected inflation over a future period, useful for assessing shifts in the expected inflation path.", | |
| "Inflation Expectation": "Expected future inflation from consumers, experts, or markets. It is an important macro signal for monetary policy expectations and long-term rates.", | |
| "Money Market": "Short-term funding market indicators related to policy rates, liquidity, and financial institutions' funding conditions.", | |
| "Mortgage Rate": "Mortgage interest rate levels, which help explain housing affordability, real estate demand, and household financial conditions.", | |
| "Term Spread": "The difference between long-term and short-term interest rates, commonly used to assess growth expectations, policy expectations, and recession risk.", | |
| "TIPS Yield": "Real yields on U.S. Treasury Inflation-Protected Securities, used to evaluate real rate conditions and real discount rates.", | |
| "Treasury Yield": "U.S. Treasury yields, representing the term structure of risk-free rates and a benchmark for asset pricing and macro-financial conditions.", | |
| Activity: "Broad measures of economic activity such as production, consumption, transport, and coincident indicators, used to detect expansion and slowdown.", | |
| Banking: "Indicators of bank lending, deposits, reserves, and credit supply, important for evaluating financial intermediation and banking system liquidity.", | |
| "Capacity Utilization by Industry": "Industry-level measures of how intensively production capacity is being used, useful for assessing spare capacity and supply-side pressure.", | |
| Credit: "Credit volumes and lending flows in the private or public sector, reflecting leverage, funding supply, and financial cycle dynamics.", | |
| "Current Account": "External transaction indicators covering goods, services, income, and transfers, used to assess external balance and medium-term currency pressure.", | |
| "Energy Activity": "Energy-sector activity such as production, consumption, inventories, and drilling, capturing supply-demand shifts in commodity markets and industry.", | |
| External: "Macro indicators related to exports, imports, external transactions, and the balance of payments, used for global demand and external vulnerability analysis.", | |
| "FDI Flows": "Foreign direct investment inflows and outflows, reflecting long-term capital movement and firms' overseas production and investment activity.", | |
| "Financial Flows": "Financial account, capital flow, and financial asset transaction indicators, used to assess global fund flows and risk appetite.", | |
| Fiscal: "Government spending, revenue, fiscal balance, and debt indicators, describing fiscal policy stance and public-sector funding needs.", | |
| Freight: "Freight and logistics activity indicators, which quickly reflect real demand, manufacturing activity, and supply-chain conditions.", | |
| GDP: "Gross domestic product and related growth indicators, the broadest measure of total production and the business cycle.", | |
| Housing: "Housing starts, permits, sales, inventories, and other housing market activity indicators, often rate-sensitive and forward-looking.", | |
| "Housing Price": "Housing price or rent indicators, important for assessing asset prices, housing affordability, and household wealth.", | |
| Income: "Household or personal income indicators, key variables for evaluating consumption capacity and labor income flows.", | |
| "Industrial Production by Industry": "Industry-level production or production index measures, used to evaluate sector-specific cyclical sensitivity and manufacturing activity.", | |
| Inflation: "Consumer prices, producer prices, and other price level indicators, directly tied to monetary policy, real income, and rate expectations.", | |
| "International Investment Position": "A country's stock of external financial assets and liabilities, used to assess net external position and external vulnerability.", | |
| Inventory: "Inventory levels and changes for firms or industries, used to assess demand slowdown, oversupply, and production adjustment risk.", | |
| Labor: "Employment, unemployment, wages, labor participation, and other labor market indicators that explain both growth and inflation pressure.", | |
| "Labor by Industry": "Industry-level employment and labor market indicators, used to evaluate labor demand by sector rather than at the aggregate level.", | |
| Money: "Money supply, liquidity, deposits, and related indicators, describing financial conditions and macro liquidity.", | |
| "Portfolio Flows": "Cross-border portfolio investment inflows and outflows in equities, bonds, and other financial assets.", | |
| Productivity: "Labor productivity or total factor productivity indicators, used to assess long-term growth potential and unit labor cost pressure.", | |
| "Regional Fed": "Manufacturing and services survey indicators from regional Federal Reserve banks, reflecting regional business conditions and sentiment.", | |
| Saving: "Household or national saving rate indicators, describing consumption propensity, fiscal capacity, and long-term capital accumulation.", | |
| Sentiment: "Consumer, business, or market participant sentiment and expectations, often useful as leading indicators.", | |
| Spending: "Personal consumption expenditures, retail sales, and other spending activity indicators, used to assess domestic demand and consumer momentum.", | |
| Stress: "Financial market or macroeconomic stress indicators, capturing market instability, liquidity strain, and downside risk.", | |
| "TIC Flows": "International capital flow indicators from U.S. Treasury International Capital data, showing foreign transactions and holdings of U.S. securities.", | |
| "Implied Volatility": "Forward-looking volatility inferred from option prices, reflecting uncertainty, risk aversion, and potential short-term shocks.", | |
| "US Equity Index": "Major U.S. equity index levels, used as broad benchmarks for stock market direction and risk asset performance.", | |
| "Equity OHLCV Close": "Close price series for listed equities, a core target for company-level price forecasting, cross-sectional ranking, and portfolio backtests.", | |
| "ETF OHLCV Close": "Close price series for exchange-traded funds, covering tradable sector, style, asset class, and thematic exposures.", | |
| "Balance Sheet": "Corporate balance sheet items such as assets, liabilities, and equity, used to assess financial health, leverage, and asset-base changes.", | |
| "Cash Flow": "Cash flow statement items such as operating cash flow and capital expenditure, showing actual cash generation and investment activity.", | |
| "EPS Basic": "Basic earnings per share, measuring profit per common share without dilution effects.", | |
| "EPS Diluted": "Diluted earnings per share, a more conservative shareholder earnings measure that accounts for potential share dilution.", | |
| "Net Income": "A company's final net profit after revenue, expenses, interest, taxes, and other items.", | |
| "Operating Income": "Profit from core operations before interest, taxes, and one-off items, used to assess core business profitability.", | |
| Revenue: "A company's top-line sales from products and services, directly reflecting growth and demand.", | |
| "Commodity ETF Broad": "Broad commodity ETF prices that provide diversified exposure to commodity markets.", | |
| "Commodity ETF Single": "ETF prices tracking a single commodity or narrow commodity exposure, such as gold or oil.", | |
| "Commodity ETF Thematic": "Thematic commodity ETF prices covering areas such as batteries, uranium, or commodity-related industries.", | |
| "Commodity Spot Bdaily": "Business-daily commodity spot or benchmark price series, used to evaluate short-term commodity price changes and price discovery.", | |
| "Commodity Spot Monthly": "Monthly commodity spot or benchmark price series, used to evaluate lower-frequency commodity trends and supply-demand shifts.", | |
| "Future Front": "Nearest-maturity commodity futures prices, reflecting tradable prices that include storage costs, rates, and supply-demand expectations.", | |
| "Inventory Stock": "Commodity inventory or stock levels, used to evaluate shortage, excess inventory, and seasonal supply-demand changes.", | |
| "Crypto Pair Close (Top 100)": "USD-denominated close price series for crypto assets, used to evaluate direction, ranking, and investment performance in volatile digital assets.", | |
| "Crypto Pair Close": "USD-denominated close price series for crypto assets, used to evaluate direction, ranking, and investment performance in volatile digital assets.", | |
| "FX Pair": "Exchange rate series between two currencies, reflecting rate differentials, trade conditions, capital flows, and risk appetite.", | |
| "Country Index": "Country-level equity market index series, used for cross-country ranking and global asset allocation analysis.", | |
| "Regional Index": "Regional equity index series covering groups of countries such as Europe, Asia, or emerging markets." | |
| }; | |
| const strengthHeaderLabel = (label) => { | |
| const words = String(label).split(/\s+/).filter(Boolean); | |
| if (words.length < 3 && String(label).length <= 16) return `<span>${label}</span>`; | |
| const mid = Math.ceil(words.length / 2); | |
| const lines = [words.slice(0, mid).join(" "), words.slice(mid).join(" ")].filter(Boolean); | |
| return `<span class="sp-stack">${lines.map((line) => `<span>${line}</span>`).join("")}</span>`; | |
| }; | |
| const finite = (v) => Number.isFinite(v); | |
| const fmt = (v, digits = 2) => finite(v) ? v.toFixed(digits) : '<span class="skip">—</span>'; | |
| const fmtValue = (v) => { | |
| if (!finite(v)) return '<span class="skip">—</span>'; | |
| const a = Math.abs(v); | |
| if (a !== 0 && (a < 1e-3 || a >= 1e5)) return v.toExponential(2); | |
| if (a >= 100) return v.toFixed(1); | |
| if (a >= 1) return v.toFixed(3); | |
| return v.toFixed(4); | |
| }; | |
| const arithmeticMean = (values) => { | |
| const valid = values.filter(finite); | |
| return valid.length ? valid.reduce((sum, value) => sum + value, 0) / valid.length : null; | |
| }; | |
| const escapeHtml = (value) => String(value ?? "") | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| const formatAoeDate = (value) => { | |
| const date = new Date(value); | |
| const aoe = new Date(date.getTime() - 12 * 60 * 60 * 1000); | |
| return `${aoe.getUTCFullYear()}-${String(aoe.getUTCMonth() + 1).padStart(2, "0")}-${String(aoe.getUTCDate()).padStart(2, "0")} ${aoe.getUTCHours() < 12 ? "AM" : "PM"} ${aoe.getUTCHours() % 12 || 12}:${String(aoe.getUTCMinutes()).padStart(2, "0")}:${String(aoe.getUTCSeconds()).padStart(2, "0")} AoE`; | |
| }; | |
| const rankBadge = (r) => { | |
| if (!finite(r)) return '<span class="skip">—</span>'; | |
| const whole = Number.isInteger(r); | |
| const cls = whole && r <= 3 ? ` g${r}` : ""; | |
| return `<span class="rankbadge${cls}">${whole ? r : r.toFixed(1)}</span>`; | |
| }; | |
| const overviewRankBadge = (rank) => { | |
| if (!finite(rank)) return '<span class="skip">—</span>'; | |
| const whole = Number.isInteger(rank); | |
| const cls = whole && rank <= 3 ? ` g${rank}` : ""; | |
| return `<span class="rankbadge${cls}">${whole ? rank : rank.toFixed(1)}</span>`; | |
| }; | |
| const overviewScoreText = (score) => { | |
| if (!finite(score)) return '<span class="skip">—</span>'; | |
| return `<span class="score-align">${fmt(score)}</span>`; | |
| }; | |
| const parseModelSize = (value) => { | |
| const raw = String(value || "").trim().toLowerCase(); | |
| if (!raw) return null; | |
| const match = raw.match(/^<?\s*([0-9]+(?:\.[0-9]+)?)\s*([kmb])$/); | |
| if (!match) return null; | |
| const amount = Number(match[1]); | |
| const unit = match[2]; | |
| if (!finite(amount)) return null; | |
| if (unit === "k") return amount / 1000; | |
| if (unit === "b") return amount * 1000; | |
| return amount; | |
| }; | |
| const sizeTickLabel = (value) => { | |
| if (value < 1) return `${Math.round(value * 1000)}K`; | |
| if (value >= 1000) return `${fmt(value / 1000, value % 1000 ? 1 : 0)}B`; | |
| return `${fmt(value, value % 1 ? 1 : 0)}M`; | |
| }; | |
| const metricBase = (metric) => { | |
| if (metric.startsWith("mase_diff")) return "mase_diff"; | |
| if (metric.startsWith("hrpp")) return "hrpp"; | |
| if (metric.startsWith("hrp")) return "hrp"; | |
| if (metric.startsWith("hr")) return "hr"; | |
| if (metric.startsWith("mase")) return "mase"; | |
| return metric.split("_")[0]; | |
| }; | |
| const metricName = (metric) => { | |
| const base = metricBase(metric); | |
| return METRIC_INFO[base]?.[0] || base; | |
| }; | |
| const metricDirection = (metric) => metric.startsWith("mase_") || metric.startsWith("vol_") ? -1 : 1; | |
| const metricLabel = (metric) => { | |
| const horizon = metric.replace(/^(hrpp|hrp|hr|mase_diff|mase|ic|return|sharpe|mdd|vol)_?/, ""); | |
| return `${metricName(metric)} · ${horizon || metric}`; | |
| }; | |
| const metricGroupKey = (metric) => metric === "mase_pct0" ? "mase_pct0" : metricBase(metric); | |
| const metricGroupLabel = (key) => { | |
| if (key === "hr") return "Hitrate"; | |
| if (key === "hrp") return "Hitrate △"; | |
| if (key === "hrpp") return "Hitrate △²"; | |
| if (key === "mase_pct0") return "MASE"; | |
| return METRIC_INFO[key]?.[0] || key; | |
| }; | |
| const metricGroupDirection = (metric) => metric === "mase_pct0" || metric === "mase_diff" || metric === "mase" || metric === "vol" ? -1 : 1; | |
| const drawerMetricName = (metric) => { | |
| const base = metricBase(metric); | |
| if (base === "hr") return "Hitrate"; | |
| if (base === "hrp") return "Hitrate △"; | |
| if (base === "hrpp") return "Hitrate △²"; | |
| return METRIC_INFO[base]?.[0] || base; | |
| }; | |
| const periodLabel = (token) => { | |
| const key = String(token || "").toLowerCase(); | |
| const map = { | |
| pct1w: "WoW", | |
| pct1m: "MoM", | |
| pct1q: "QoQ", | |
| pct1h: "HoH", | |
| pct1y: "YoY", | |
| h1w: "WoW", | |
| h1m: "MoM", | |
| h1q: "QoQ", | |
| h1h: "HoH", | |
| h1y: "YoY" | |
| }; | |
| return map[key] || token || "—"; | |
| }; | |
| const targetLabel = (token) => { | |
| const key = String(token || "").toLowerCase(); | |
| const match = key.match(/^after(\d+)([wmqhy])$/); | |
| return match ? `${match[1]}${match[2].toUpperCase()}` : "—"; | |
| }; | |
| const optionNone = "__none__"; | |
| const predTargetLabel = (token) => token === optionNone ? "Original" : targetLabel(token); | |
| const typeLabel = (token) => { | |
| const key = String(token || "").toLowerCase(); | |
| const map = { | |
| pct1w: "WoW", | |
| pct1m: "MoM", | |
| pct1q: "QoQ", | |
| pct1h: "HoH", | |
| pct1y: "YoY", | |
| h1w: "WoW", | |
| h1m: "MoM", | |
| h1q: "QoQ", | |
| h1h: "HoH", | |
| h1y: "YoY", | |
| [optionNone]: "All" | |
| }; | |
| return map[key] || token || "All"; | |
| }; | |
| const optionOrder = (token) => { | |
| const key = String(token || "").toLowerCase(); | |
| if (key === optionNone) return 999; | |
| const pctOrder = { pct1w: 0, pct1m: 1, pct1q: 2, pct1h: 3, pct1y: 4 }; | |
| const horizonOrderMap = { h1w: 10, h1m: 11, h1q: 12, h1h: 13, h1y: 14 }; | |
| if (key in pctOrder) return pctOrder[key]; | |
| if (key in horizonOrderMap) return horizonOrderMap[key]; | |
| const match = key.match(/^after1([wmqhy])$/); | |
| if (match) return ({ w: 0, m: 1, q: 2, h: 3, y: 4 }[match[1]] ?? 99); | |
| return 99; | |
| }; | |
| const targetFromType = (typeToken) => { | |
| const match = String(typeToken || "").toLowerCase().match(/^h1([wmqhy])$/); | |
| return match ? `after1${match[1]}` : ""; | |
| }; | |
| const metricOptionTokens = (metric) => { | |
| const base = metricBase(metric); | |
| if (base === "mase" && metric === "mase_pct0") return { type: optionNone, target: optionNone }; | |
| const rest = metric.replace(new RegExp(`^${base}_?`), ""); | |
| const [type, target] = rest.split("_"); | |
| return { | |
| type: type || optionNone, | |
| target: target || targetFromType(type) || optionNone | |
| }; | |
| }; | |
| const freqCode = (frequency) => { | |
| const map = { | |
| "Annual": "Y", | |
| "Business-daily": "BD", | |
| "Daily": "D", | |
| "Monthly": "M", | |
| "Quarterly": "Q", | |
| "Weekly": "W" | |
| }; | |
| return map[frequency] || frequency || ""; | |
| }; | |
| const horizonOrder = (value) => { | |
| const key = String(value || "").toLowerCase(); | |
| const match = key.match(/(?:pct|after|h)(\d+)([wmqhy])$/); | |
| if (!match) return 99; | |
| const unitOrder = { w: 0, m: 1, q: 2, h: 3, y: 4 }; | |
| return (Number(match[1]) * 10) + (unitOrder[match[2]] ?? 9); | |
| }; | |
| const parseMetricParts = (metric) => { | |
| const base = metricBase(metric); | |
| if (base === "mase" && metric === "mase_pct0") { | |
| return { name: drawerMetricName(metric), type: typeLabel(optionNone), target: predTargetLabel(optionNone) }; | |
| } | |
| const rest = metric.replace(new RegExp(`^${base}_?`), ""); | |
| const [typeToken, targetToken] = rest.split("_"); | |
| const derivedTarget = targetToken || targetFromType(typeToken); | |
| return { | |
| name: drawerMetricName(metric), | |
| type: periodLabel(typeToken), | |
| target: predTargetLabel(derivedTarget || optionNone) | |
| }; | |
| }; | |
| const rankText = (rank, total) => { | |
| if (!finite(rank)) return "—"; | |
| return `${rankBadge(rank)}<small class="rank-total">/ ${total.toLocaleString()}</small>`; | |
| }; | |
| const metricFilterKey = (row) => `${row.tier}|${metricGroupKey(row.metric)}`; | |
| const includedRecords = ({ model = true, metric = true, option = true, detail = true, frequency = true } = {}) => state.records.filter((r) => { | |
| const options = metricOptionTokens(r.metric); | |
| return (!model || !state.excludedModelTypes.has(modelType(r.model))) | |
| && (!metric || !state.excludedMetrics.has(metricFilterKey(r))) | |
| && (!option || (!state.excludedTypes.has(options.type) && !state.excludedTargets.has(options.target))) | |
| && (!detail || !state.excludedDetails.has(r.detail_category)) | |
| && (!frequency || !state.excludedFrequencies.has(r.frequency)); | |
| }); | |
| const geometricMean = (values) => { | |
| const valid = values.filter((v) => finite(v) && v > 0); | |
| return valid.length ? Math.exp(valid.reduce((s, v) => s + Math.log(v), 0) / valid.length) : null; | |
| }; | |
| const rankSum = (values) => { | |
| const valid = values.filter(finite); | |
| return valid.length ? valid.reduce((sum, value) => sum + value, 0) : null; | |
| }; | |
| const compareNullable = (a, b, asc = true) => { | |
| const af = finite(a); | |
| const bf = finite(b); | |
| if (!af && !bf) return 0; | |
| if (!af) return 1; | |
| if (!bf) return -1; | |
| return asc ? a - b : b - a; | |
| }; | |
| function rankRows(rows, valueKey, direction = -1) { | |
| const rankValue = (value) => Math.round(value * 10000) / 10000; | |
| const sorted = rows.filter((r) => finite(r[valueKey])).sort((a, b) => direction > 0 ? rankValue(b[valueKey]) - rankValue(a[valueKey]) : rankValue(a[valueKey]) - rankValue(b[valueKey])); | |
| for (let i = 0; i < sorted.length;) { | |
| let j = i + 1; | |
| while (j < sorted.length && rankValue(sorted[j][valueKey]) === rankValue(sorted[i][valueKey])) j += 1; | |
| const rank = (i + 1 + j) / 2; | |
| for (let k = i; k < j; k += 1) sorted[k].rank = rank; | |
| i = j; | |
| } | |
| return sorted; | |
| } | |
| function tierStats(records = includedRecords()) { | |
| const byModelTier = new Map(); | |
| for (const row of records) { | |
| const key = `${row.model}|${row.tier}`; | |
| if (!byModelTier.has(key)) byModelTier.set(key, []); | |
| byModelTier.get(key).push(row.rank); | |
| } | |
| const models = includedModels(); | |
| const stats = new Map(models.map((model) => [model, { model, count: 0, pointwise_k: null, ic_k: null, portfolio_k: null }])); | |
| for (const model of models) { | |
| const count = records.filter((r) => r.model === model).length; | |
| const target = stats.get(model); | |
| target.count = count; | |
| for (const tier of Object.keys(ASPECT)) target[`${ASPECT[tier]}_k`] = geometricMean(byModelTier.get(`${model}|${tier}`) || []); | |
| } | |
| for (const aspect of ["pointwise", "ic", "portfolio"]) { | |
| const ranked = rankRows([...stats.values()].map((row) => ({ model: row.model, value: row[`${aspect}_k`] })), "value", -1); | |
| for (const row of ranked) stats.get(row.model)[aspect] = row.rank; | |
| } | |
| return [...stats.values()].map((row) => ({ ...row, overall: rankSum([row.pointwise, row.ic, row.portfolio]) })); | |
| } | |
| function sortedOverview() { | |
| const rows = (state.recordsReady ? tierStats() : [...(state.data?.overall || [])]).map((row) => ({ | |
| ...row, | |
| overall_k: geometricMean([row.pointwise_k, row.ic_k, row.portfolio_k]) | |
| })); | |
| const s = state.sort; | |
| const valueForSort = (row, key) => { | |
| if (key === "pos") return row.overall; | |
| if (key === "model_type") return modelTypeLabel(modelType(row.model)); | |
| if (key === "org") return modelOrg(row.model); | |
| if (key === "model_size") return parseModelSize(modelSize(row.model)); | |
| if (key === "submit_time") return modelSubmitTime(row.model); | |
| return row[key]; | |
| }; | |
| rows.sort((a, b) => { | |
| if (s.key === "model") return s.asc ? displayName(a.model).localeCompare(displayName(b.model)) : displayName(b.model).localeCompare(displayName(a.model)); | |
| if (s.key === "model_type" || s.key === "org" || s.key === "submit_time") { | |
| const av = valueForSort(a, s.key); | |
| const bv = valueForSort(b, s.key); | |
| return s.asc ? av.localeCompare(bv) : bv.localeCompare(av); | |
| } | |
| const av = valueForSort(a, s.key); | |
| const bv = valueForSort(b, s.key); | |
| return compareNullable(av, bv, s.asc); | |
| }); | |
| return rows; | |
| } | |
| function detailsByScope() { | |
| const grouped = new Map(); | |
| for (const row of state.records) { | |
| const group = `${row.scope || "Other"} · ${row.category || "Other"}`; | |
| if (!grouped.has(group)) grouped.set(group, new Map()); | |
| if (!grouped.get(group).has(row.detail_category)) grouped.get(group).set(row.detail_category, displayDetailLabel(row.detail_category)); | |
| } | |
| return [...grouped.entries()].sort((a, b) => a[0].localeCompare(b[0])); | |
| } | |
| function allFrequencies() { | |
| return [...new Set(state.records.map((row) => row.frequency).filter(Boolean))].sort(); | |
| } | |
| function metricGroupsByTier() { | |
| const order = new Map(["hr", "hrp", "hrpp", "mase", "mase_pct0", "mase_diff", "ic", "return", "sharpe", "mdd", "vol"].map((key, index) => [key, index])); | |
| const grouped = new Map(); | |
| for (const row of state.records) { | |
| const key = metricGroupKey(row.metric); | |
| if (!grouped.has(row.tier)) grouped.set(row.tier, new Set()); | |
| grouped.get(row.tier).add(key); | |
| } | |
| return Object.keys(TIER_LABEL).map((tier) => { | |
| const metrics = [...(grouped.get(tier) || [])].sort((a, b) => (order.get(a) ?? 99) - (order.get(b) ?? 99) || a.localeCompare(b)); | |
| return [tier, metrics]; | |
| }); | |
| } | |
| function metricOptionGroups() { | |
| const types = new Set(); | |
| const targets = new Set(); | |
| for (const row of state.records) { | |
| const options = metricOptionTokens(row.metric); | |
| types.add(options.type); | |
| targets.add(options.target); | |
| } | |
| return { | |
| types: [...types].sort((a, b) => optionOrder(a) - optionOrder(b) || typeLabel(a).localeCompare(typeLabel(b))), | |
| targets: [...targets].sort((a, b) => optionOrder(a) - optionOrder(b) || predTargetLabel(a).localeCompare(predTargetLabel(b))) | |
| }; | |
| } | |
| function modelTypeGroups() { | |
| return [...new Set(state.models.map(modelType))] | |
| .sort((a, b) => modelTypeLabel(a).localeCompare(modelTypeLabel(b))); | |
| } | |
| function optionButtonGroups(values, labeler, mergeByLabel = false) { | |
| if (!mergeByLabel) { | |
| return values.map((value) => ({ | |
| label: labeler(value), | |
| tokens: [value], | |
| title: value === optionNone ? "No option" : value | |
| })); | |
| } | |
| const groups = new Map(); | |
| for (const value of values) { | |
| const label = labeler(value); | |
| if (!groups.has(label)) groups.set(label, []); | |
| groups.get(label).push(value); | |
| } | |
| return [...groups.entries()] | |
| .map(([label, tokens]) => ({ | |
| label, | |
| tokens, | |
| title: tokens.map((value) => value === optionNone ? "No option" : value).join(", ") | |
| })) | |
| .sort((a, b) => optionOrder(a.tokens[0]) - optionOrder(b.tokens[0]) || a.label.localeCompare(b.label)); | |
| } | |
| function buildOptionSubcard({ container, key, title, values, excluded, labeler, mergeByLabel = false }) { | |
| const buttonGroups = optionButtonGroups(values, labeler, mergeByLabel); | |
| const allTokens = buttonGroups.flatMap((group) => group.tokens); | |
| const groupAllOn = allTokens.every((value) => !excluded.has(value)); | |
| const subcardKey = `option|${key}`; | |
| const collapsed = state.collapsedSubcards.has(subcardKey); | |
| const wrap = document.createElement("div"); | |
| wrap.className = `spec-group spec-subcard${collapsed ? " subcard-collapsed" : ""}`; | |
| wrap.innerHTML = ` | |
| <div class="spec-group-head"> | |
| <button class="spec-group-label subcard-trigger" type="button" aria-expanded="${!collapsed}"> | |
| <span>${title}</span> | |
| <span class="collapse-mark" aria-hidden="true">${collapsed ? "+" : "-"}</span> | |
| </button> | |
| <button class="ghost-btn group-toggle" type="button">${groupAllOn ? "Deselect all" : "Select all"}</button> | |
| </div> | |
| <div class="spec-chips"></div> | |
| `; | |
| wrap.querySelector(".subcard-trigger").onclick = () => { | |
| if (state.collapsedSubcards.has(subcardKey)) state.collapsedSubcards.delete(subcardKey); | |
| else state.collapsedSubcards.add(subcardKey); | |
| buildSpecToggles(); | |
| }; | |
| wrap.querySelector(".group-toggle").onclick = () => { | |
| for (const value of allTokens) { | |
| if (groupAllOn) excluded.add(value); | |
| else excluded.delete(value); | |
| } | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| const chips = wrap.querySelector(".spec-chips"); | |
| for (const option of buttonGroups) { | |
| const allOn = option.tokens.every((value) => !excluded.has(value)); | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| if (key === "model-type") btn.innerHTML = renderModelTypeFilterLabel(option.tokens[0], option.label); | |
| else btn.textContent = option.label; | |
| btn.title = option.title; | |
| btn.className = `filter-chip ${allOn ? "on" : "off"}`; | |
| btn.onclick = () => { | |
| for (const value of option.tokens) { | |
| if (allOn) excluded.add(value); | |
| else excluded.delete(value); | |
| } | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| chips.appendChild(btn); | |
| } | |
| container.appendChild(wrap); | |
| } | |
| function buildSpecToggles() { | |
| const modelBox = $("model-toggles"); | |
| const metricBox = $("metric-toggles"); | |
| const optionBox = $("metric-option-toggles"); | |
| const box = $("spec-toggles"); | |
| const freqBox = $("frequency-toggles"); | |
| if (!state.recordsReady) { | |
| modelBox.innerHTML = '<p class="loading-note">Loading model configs...</p>'; | |
| metricBox.innerHTML = '<p class="loading-note">Loading detailed records...</p>'; | |
| optionBox.innerHTML = '<p class="loading-note">Loading detailed records...</p>'; | |
| box.innerHTML = '<p class="loading-note">Loading detailed records...</p>'; | |
| freqBox.innerHTML = '<p class="loading-note">Loading detailed records...</p>'; | |
| $("toggle-all-models").disabled = true; | |
| $("toggle-all-metrics").disabled = true; | |
| $("toggle-all-metric-options").disabled = true; | |
| $("toggle-all-specs").disabled = true; | |
| $("toggle-all-frequencies").disabled = true; | |
| return; | |
| } | |
| $("toggle-all-models").disabled = false; | |
| $("toggle-all-metrics").disabled = false; | |
| $("toggle-all-metric-options").disabled = false; | |
| $("toggle-all-specs").disabled = false; | |
| $("toggle-all-frequencies").disabled = false; | |
| modelBox.innerHTML = ""; | |
| metricBox.innerHTML = ""; | |
| optionBox.innerHTML = ""; | |
| box.innerHTML = ""; | |
| freqBox.innerHTML = ""; | |
| buildOptionSubcard({ | |
| container: modelBox, | |
| key: "model-type", | |
| title: "Model Type", | |
| values: modelTypeGroups(), | |
| excluded: state.excludedModelTypes, | |
| labeler: modelTypeLabel | |
| }); | |
| const modelTypes = modelTypeGroups(); | |
| const allModelTypesOn = modelTypes.every((type) => !state.excludedModelTypes.has(type)); | |
| $("toggle-all-models").textContent = allModelTypesOn ? "Deselect all" : "Select all"; | |
| $("toggle-all-models").onclick = () => { | |
| state.excludedModelTypes = allModelTypesOn ? new Set(modelTypes) : new Set(); | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| const metricGroups = metricGroupsByTier(); | |
| const presentMetrics = metricGroups.flatMap(([tier, metrics]) => metrics.map((metric) => `${tier}|${metric}`)); | |
| for (const [tier, metrics] of metricGroups) { | |
| const metricKeys = metrics.map((metric) => `${tier}|${metric}`); | |
| const groupAllOn = metricKeys.every((key) => !state.excludedMetrics.has(key)); | |
| const subcardKey = `metric|${tier}`; | |
| const collapsed = state.collapsedSubcards.has(subcardKey); | |
| const wrap = document.createElement("div"); | |
| wrap.className = `spec-group spec-subcard${collapsed ? " subcard-collapsed" : ""}`; | |
| wrap.innerHTML = ` | |
| <div class="spec-group-head"> | |
| <button class="spec-group-label subcard-trigger" type="button" aria-expanded="${!collapsed}"> | |
| <span>${TIER_SHORT[tier]}</span> | |
| <span class="collapse-mark" aria-hidden="true">${collapsed ? "+" : "-"}</span> | |
| </button> | |
| <button class="ghost-btn group-toggle" type="button">${groupAllOn ? "Deselect all" : "Select all"}</button> | |
| </div> | |
| <div class="spec-chips"></div> | |
| `; | |
| wrap.querySelector(".subcard-trigger").onclick = () => { | |
| if (state.collapsedSubcards.has(subcardKey)) state.collapsedSubcards.delete(subcardKey); | |
| else state.collapsedSubcards.add(subcardKey); | |
| buildSpecToggles(); | |
| }; | |
| wrap.querySelector(".group-toggle").onclick = () => { | |
| for (const key of metricKeys) { | |
| if (groupAllOn) state.excludedMetrics.add(key); | |
| else state.excludedMetrics.delete(key); | |
| } | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| const chips = wrap.querySelector(".spec-chips"); | |
| for (const metric of metrics) { | |
| const key = `${tier}|${metric}`; | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.textContent = metricGroupLabel(metric); | |
| btn.title = metric; | |
| btn.className = `filter-chip ${state.excludedMetrics.has(key) ? "off" : "on"}`; | |
| if (btn.textContent.length > 18) btn.classList.add("wide"); | |
| btn.onclick = () => { | |
| if (state.excludedMetrics.has(key)) state.excludedMetrics.delete(key); | |
| else state.excludedMetrics.add(key); | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| chips.appendChild(btn); | |
| } | |
| metricBox.appendChild(wrap); | |
| } | |
| const allMetricsOn = presentMetrics.every((key) => !state.excludedMetrics.has(key)); | |
| $("toggle-all-metrics").textContent = allMetricsOn ? "Deselect all" : "Select all"; | |
| $("toggle-all-metrics").onclick = () => { | |
| state.excludedMetrics = allMetricsOn ? new Set(presentMetrics) : new Set(); | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| const optionGroups = metricOptionGroups(); | |
| buildOptionSubcard({ | |
| container: optionBox, | |
| key: "type", | |
| title: "Target Type", | |
| values: optionGroups.types, | |
| excluded: state.excludedTypes, | |
| labeler: typeLabel, | |
| mergeByLabel: true | |
| }); | |
| buildOptionSubcard({ | |
| container: optionBox, | |
| key: "target", | |
| title: "Pred. Target", | |
| values: optionGroups.targets, | |
| excluded: state.excludedTargets, | |
| labeler: predTargetLabel | |
| }); | |
| const allMetricOptionsOn = optionGroups.types.every((type) => !state.excludedTypes.has(type)) | |
| && optionGroups.targets.every((target) => !state.excludedTargets.has(target)); | |
| $("toggle-all-metric-options").textContent = allMetricOptionsOn ? "Deselect all" : "Select all"; | |
| $("toggle-all-metric-options").onclick = () => { | |
| state.excludedTypes = allMetricOptionsOn ? new Set(optionGroups.types) : new Set(); | |
| state.excludedTargets = allMetricOptionsOn ? new Set(optionGroups.targets) : new Set(); | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| const groups = detailsByScope(); | |
| const present = groups.flatMap(([, values]) => [...values.keys()]); | |
| for (const [group, values] of groups) { | |
| const groupDetails = [...values.keys()]; | |
| const groupAllOn = groupDetails.every((detail) => !state.excludedDetails.has(detail)); | |
| const subcardKey = `detail|${group}`; | |
| const collapsed = state.collapsedSubcards.has(subcardKey); | |
| const wrap = document.createElement("div"); | |
| wrap.className = `spec-group spec-subcard${collapsed ? " subcard-collapsed" : ""}`; | |
| wrap.innerHTML = ` | |
| <div class="spec-group-head"> | |
| <button class="spec-group-label subcard-trigger" type="button" aria-expanded="${!collapsed}"> | |
| <span>${group}</span> | |
| <span class="collapse-mark" aria-hidden="true">${collapsed ? "+" : "-"}</span> | |
| </button> | |
| <button class="ghost-btn group-toggle" type="button">${groupAllOn ? "Deselect all" : "Select all"}</button> | |
| </div> | |
| <div class="spec-chips"></div> | |
| `; | |
| wrap.querySelector(".subcard-trigger").onclick = () => { | |
| if (state.collapsedSubcards.has(subcardKey)) state.collapsedSubcards.delete(subcardKey); | |
| else state.collapsedSubcards.add(subcardKey); | |
| buildSpecToggles(); | |
| }; | |
| wrap.querySelector(".group-toggle").onclick = () => { | |
| for (const detail of groupDetails) { | |
| if (groupAllOn) state.excludedDetails.add(detail); | |
| else state.excludedDetails.delete(detail); | |
| } | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| const chips = wrap.querySelector(".spec-chips"); | |
| for (const [detail, label] of [...values.entries()].sort((a, b) => a[1].localeCompare(b[1]))) { | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.textContent = label; | |
| btn.title = detail; | |
| btn.className = `filter-chip ${state.excludedDetails.has(detail) ? "off" : "on"}`; | |
| if (label.length > 18) btn.classList.add("wide"); | |
| btn.onclick = () => { | |
| if (state.excludedDetails.has(detail)) state.excludedDetails.delete(detail); | |
| else state.excludedDetails.add(detail); | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| chips.appendChild(btn); | |
| } | |
| box.appendChild(wrap); | |
| } | |
| const allOn = present.every((detail) => !state.excludedDetails.has(detail)); | |
| $("toggle-all-specs").textContent = allOn ? "Deselect all" : "Select all"; | |
| $("toggle-all-specs").onclick = () => { | |
| state.excludedDetails = allOn ? new Set(present) : new Set(); | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| const frequencies = allFrequencies(); | |
| for (const frequency of frequencies) { | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.textContent = frequency; | |
| btn.className = `filter-chip ${state.excludedFrequencies.has(frequency) ? "off" : "on"}`; | |
| btn.onclick = () => { | |
| if (state.excludedFrequencies.has(frequency)) state.excludedFrequencies.delete(frequency); | |
| else state.excludedFrequencies.add(frequency); | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| freqBox.appendChild(btn); | |
| } | |
| const allFreqOn = frequencies.every((frequency) => !state.excludedFrequencies.has(frequency)); | |
| $("toggle-all-frequencies").textContent = allFreqOn ? "Deselect all" : "Select all"; | |
| $("toggle-all-frequencies").onclick = () => { | |
| state.excludedFrequencies = allFreqOn ? new Set(frequencies) : new Set(); | |
| buildSpecToggles(); | |
| render(); | |
| }; | |
| } | |
| function setupCollapsibles() { | |
| document.querySelectorAll(".collapse-trigger").forEach((button) => { | |
| const target = $(button.dataset.collapseTarget); | |
| if (!target) return; | |
| const sync = () => { | |
| const collapsed = state.collapsedFilters.has(button.dataset.collapseTarget); | |
| target.classList.toggle("collapsed", collapsed); | |
| button.setAttribute("aria-expanded", String(!collapsed)); | |
| button.querySelector(".collapse-mark").textContent = collapsed ? "+" : "-"; | |
| }; | |
| button.onclick = () => { | |
| if (state.collapsedFilters.has(button.dataset.collapseTarget)) state.collapsedFilters.delete(button.dataset.collapseTarget); | |
| else state.collapsedFilters.add(button.dataset.collapseTarget); | |
| sync(); | |
| }; | |
| sync(); | |
| }); | |
| } | |
| function setupTabs() { | |
| document.querySelectorAll("#viewtabs button").forEach((button) => { | |
| button.onclick = () => { | |
| state.view = button.dataset.view; | |
| document.querySelectorAll("#viewtabs button").forEach((item) => item.classList.toggle("active", item === button)); | |
| document.querySelectorAll("main > .panel").forEach((panel) => panel.hidden = panel.id !== `panel-${state.view}`); | |
| syncSidebarForView(); | |
| render(); | |
| }; | |
| }); | |
| } | |
| function syncSidebarForView() { | |
| const showSidebar = state.view === "overview" || state.view === "overview-plot" || state.view === "metrics" || state.view === "strength"; | |
| $("sidebar").hidden = !showSidebar; | |
| $("model-filter-card").hidden = !showSidebar; | |
| $("metric-filter-card").hidden = !showSidebar; | |
| $("metric-option-card").hidden = !showSidebar; | |
| } | |
| function renderLoadingPanel(targetId) { | |
| $(targetId).innerHTML = '<tr><td class="skip" colspan="4">Loading detailed records. The overview is shown first.</td></tr>'; | |
| } | |
| function renderOrganizationLegend() { | |
| const orgs = [...new Set(includedModels().map(organizationName))].sort(); | |
| $("organization-legend-body").innerHTML = orgs | |
| .map((org) => `<span><i style="background:${organizationColor(org)}"></i><b>${escapeHtml(org)}</b></span>`) | |
| .join(""); | |
| } | |
| function renderOverviewPlot(rows) { | |
| const target = $("overview-plot-body"); | |
| const points = rows | |
| .map((row) => ({ | |
| model: row.model, | |
| label: displayName(row.model), | |
| org: organizationName(row.model), | |
| sizeLabel: modelSize(row.model), | |
| size: parseModelSize(modelSize(row.model)), | |
| pointwise: row.pointwise, | |
| ic: row.ic, | |
| portfolio: row.portfolio, | |
| rank: row.overall | |
| })) | |
| .filter((row) => finite(row.size) && row.size > 0 && finite(row.rank)); | |
| if (!points.length) { | |
| target.innerHTML = '<p class="loading-note">No model size metadata is available for the selected models.</p>'; | |
| return; | |
| } | |
| const width = 860; | |
| const height = 360; | |
| const pad = { left: 58, right: 24, top: 22, bottom: 48 }; | |
| const minX = Math.min(...points.map((p) => p.size)); | |
| const maxX = Math.max(...points.map((p) => p.size)); | |
| const minY = Math.min(...points.map((p) => p.rank)); | |
| const maxY = Math.max(...points.map((p) => p.rank)); | |
| const logMin = Math.log10(Math.max(minX * 0.8, 0.1)); | |
| const logMax = Math.log10(maxX * 1.2); | |
| const yMin = Math.max(0, minY - 3); | |
| const yMax = maxY + 3; | |
| const x = (value) => pad.left + ((Math.log10(value) - logMin) / (logMax - logMin || 1)) * (width - pad.left - pad.right); | |
| const y = (value) => pad.top + ((value - yMin) / (yMax - yMin || 1)) * (height - pad.top - pad.bottom); | |
| const xTicks = [0.5, 1, 10, 100, 1000, 2500].filter((tick) => tick >= minX * 0.75 && tick <= maxX * 1.35); | |
| const yTicks = Array.from({ length: 5 }, (_, index) => yMin + ((yMax - yMin) * index) / 4); | |
| const byOrg = new Map(); | |
| for (const point of points) { | |
| if (!byOrg.has(point.org)) byOrg.set(point.org, []); | |
| byOrg.get(point.org).push(point); | |
| } | |
| const lines = [...byOrg.entries()].map(([org, group]) => { | |
| if (org === "Unspecified") return ""; | |
| const sorted = group.slice().sort((a, b) => a.size - b.size); | |
| if (sorted.length < 2) return ""; | |
| return `<polyline class="plot-line" points="${sorted.map((p) => `${x(p.size).toFixed(1)},${y(p.rank).toFixed(1)}`).join(" ")}" stroke="${organizationColor(org)}" />`; | |
| }).join(""); | |
| const tooltipRows = (p) => [ | |
| ["Model", p.label], | |
| ["Org", p.org], | |
| ["Model size", p.sizeLabel], | |
| ["Point rank", fmt(p.pointwise, finite(p.pointwise) && p.pointwise % 1 ? 1 : 0)], | |
| ["Ranking rank", fmt(p.ic, finite(p.ic) && p.ic % 1 ? 1 : 0)], | |
| ["Decision rank", fmt(p.portfolio, finite(p.portfolio) && p.portfolio % 1 ? 1 : 0)], | |
| ["Rank sum", fmt(p.rank, p.rank % 1 ? 1 : 0)] | |
| ]; | |
| const bestRank = Math.min(...points.map((p) => p.rank)); | |
| const dots = points.map((p) => ` | |
| <g class="plot-point" transform="translate(${x(p.size).toFixed(1)} ${y(p.rank).toFixed(1)})" data-tooltip='${escapeHtml(JSON.stringify(tooltipRows(p)))}'> | |
| ${p.rank === bestRank | |
| ? `<text class="plot-star" text-anchor="middle" dominant-baseline="central" fill="${organizationColor(p.org)}">★</text><text class="plot-best-label" x="12" y="-10">${escapeHtml(p.label)}</text>` | |
| : `<circle r="5.5" fill="${organizationColor(p.org)}"></circle>`} | |
| </g> | |
| `).join(""); | |
| target.innerHTML = ` | |
| <div class="plot-scroll"> | |
| <svg class="rank-size-plot" viewBox="0 0 ${width} ${height}" role="img" aria-label="Model size and Rank sum scatter plot"> | |
| ${xTicks.map((tick) => `<line class="plot-grid" x1="${x(tick)}" x2="${x(tick)}" y1="${pad.top}" y2="${height - pad.bottom}" />`).join("")} | |
| ${yTicks.map((tick) => `<line class="plot-grid" x1="${pad.left}" x2="${width - pad.right}" y1="${y(tick)}" y2="${y(tick)}" />`).join("")} | |
| <line class="plot-axis" x1="${pad.left}" x2="${width - pad.right}" y1="${height - pad.bottom}" y2="${height - pad.bottom}" /> | |
| <line class="plot-axis" x1="${pad.left}" x2="${pad.left}" y1="${pad.top}" y2="${height - pad.bottom}" /> | |
| ${xTicks.map((tick) => `<text class="plot-label" x="${x(tick)}" y="${height - 20}" text-anchor="middle">${sizeTickLabel(tick)}</text>`).join("")} | |
| ${yTicks.map((tick) => `<text class="plot-label" x="${pad.left - 10}" y="${y(tick) + 4}" text-anchor="end">${fmt(tick, 0)}</text>`).join("")} | |
| <text class="plot-axis-title" x="${width / 2}" y="${height - 4}" text-anchor="middle">Model size, parameters (log scale)</text> | |
| <text class="plot-axis-title" x="-${height / 2}" y="16" transform="rotate(-90)" text-anchor="middle">Rank sum</text> | |
| ${lines} | |
| ${dots} | |
| </svg> | |
| </div> | |
| <div class="plot-tooltip" hidden></div> | |
| `; | |
| const tooltip = target.querySelector(".plot-tooltip"); | |
| target.querySelectorAll(".plot-point").forEach((point) => { | |
| point.addEventListener("mouseenter", () => { | |
| const rows = JSON.parse(point.dataset.tooltip); | |
| tooltip.innerHTML = `<table>${rows.map(([key, value]) => `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(value)}</td></tr>`).join("")}</table>`; | |
| tooltip.hidden = false; | |
| }); | |
| point.addEventListener("mousemove", (event) => { | |
| const rect = target.getBoundingClientRect(); | |
| const left = event.clientX - rect.left - tooltip.offsetWidth - 12; | |
| tooltip.style.left = `${Math.max(8, left)}px`; | |
| tooltip.style.top = `${event.clientY - rect.top + 12}px`; | |
| }); | |
| point.addEventListener("mouseleave", () => { | |
| tooltip.hidden = true; | |
| }); | |
| }); | |
| } | |
| function renderMetricProgressPlots() { | |
| const target = $("metric-progress-body"); | |
| if (!state.recordsReady) { | |
| target.innerHTML = '<p class="loading-note">Loading detailed records.</p>'; | |
| return; | |
| } | |
| const models = includedModels().filter((model) => /^\d{4}-\d{2}-\d{2}$/.test(modelSubmitTime(model))); | |
| if (!models.length) { | |
| target.innerHTML = '<p class="loading-note">No dated model tier K data matches the selected filters.</p>'; | |
| return; | |
| } | |
| const statsByModel = new Map(tierStats().map((row) => [row.model, row])); | |
| const tiers = [ | |
| { key: "pointwise_k", title: "Point rank", tier: "tier1" }, | |
| { key: "ic_k", title: "Ranking rank", tier: "tier2" }, | |
| { key: "portfolio_k", title: "Decision rank", tier: "tier3" } | |
| ]; | |
| const width = 860; | |
| const height = 300; | |
| const pad = { left: 74, right: 24, top: 22, bottom: 48 }; | |
| const dateValue = (model) => Date.parse(`${modelSubmitTime(model)}T00:00:00Z`); | |
| const allDates = [...new Set(models.map((model) => dateValue(model)).filter(finite))].sort((a, b) => a - b); | |
| if (!allDates.length) { | |
| target.innerHTML = '<p class="loading-note">No submit date metadata is available for the selected models.</p>'; | |
| return; | |
| } | |
| const minDate = allDates[0]; | |
| const maxDate = allDates[allDates.length - 1]; | |
| const x = (date) => pad.left + ((date - minDate) / (maxDate - minDate || 1)) * (width - pad.left - pad.right); | |
| const dateLabel = (date) => new Date(date).toISOString().slice(0, 10); | |
| const dateTicks = allDates.length <= 3 ? allDates : [allDates[0], allDates[Math.floor(allDates.length / 2)], allDates[allDates.length - 1]]; | |
| const cards = tiers.map((tierInfo) => { | |
| const modelValues = models.map((model) => ({ | |
| model, | |
| date: dateValue(model), | |
| value: statsByModel.get(model)?.[tierInfo.key] | |
| })).filter((row) => finite(row.value)).sort((a, b) => a.date - b.date || displayName(a.model).localeCompare(displayName(b.model))); | |
| const timeline = []; | |
| let best = null; | |
| for (const date of allDates) { | |
| for (const row of modelValues.filter((item) => item.date === date)) { | |
| if (!best || row.value < best.value) best = row; | |
| } | |
| if (best) timeline.push({ date, value: best.value, model: best.model, org: organizationName(best.model) }); | |
| } | |
| if (timeline.length < 1) return ""; | |
| const values = timeline.map((p) => p.value); | |
| const rawMin = Math.min(...values); | |
| const rawMax = Math.max(...values); | |
| const spread = rawMax - rawMin || Math.max(Math.abs(rawMax) * 0.1, 1); | |
| const yMin = rawMin - spread * 0.12; | |
| const yMax = rawMax + spread * 0.12; | |
| const y = (value) => pad.top + ((value - yMin) / (yMax - yMin || 1)) * (height - pad.top - pad.bottom); | |
| const yTicks = [yMin, (yMin + yMax) / 2, yMax]; | |
| const segments = timeline.slice(1).map((p, index) => { | |
| const prev = timeline[index]; | |
| return `<line class="metric-progress-line" x1="${x(prev.date).toFixed(1)}" y1="${y(prev.value).toFixed(1)}" x2="${x(p.date).toFixed(1)}" y2="${y(p.value).toFixed(1)}" stroke="${organizationColor(p.org)}" />`; | |
| }).join(""); | |
| const pointTimeline = timeline.filter((p, index) => { | |
| if (index === 0 || index === timeline.length - 1) return true; | |
| return p.value !== timeline[index - 1].value; | |
| }); | |
| const points = pointTimeline.map((p) => { | |
| const rows = [ | |
| ["Date", dateLabel(p.date)], | |
| ["Tier", tierInfo.title], | |
| ["Best model", displayName(p.model)], | |
| ["Org", p.org], | |
| ["K score", fmt(p.value)] | |
| ]; | |
| return `<g class="metric-progress-point" transform="translate(${x(p.date).toFixed(1)} ${y(p.value).toFixed(1)})" data-tooltip='${escapeHtml(JSON.stringify(rows))}'><circle r="4.5" fill="${organizationColor(p.org)}"></circle></g>`; | |
| }).join(""); | |
| return ` | |
| <section class="metric-progress-card"> | |
| <h3>${tierInfo.title} <span>lower K is better</span></h3> | |
| <svg class="metric-progress-plot" viewBox="0 0 ${width} ${height}" role="img" aria-label="${tierInfo.title} K progress plot"> | |
| ${dateTicks.map((date) => `<line class="plot-grid" x1="${x(date)}" x2="${x(date)}" y1="${pad.top}" y2="${height - pad.bottom}" />`).join("")} | |
| ${yTicks.map((tick) => `<line class="plot-grid" x1="${pad.left}" x2="${width - pad.right}" y1="${y(tick)}" y2="${y(tick)}" />`).join("")} | |
| <line class="plot-axis" x1="${pad.left}" x2="${width - pad.right}" y1="${height - pad.bottom}" y2="${height - pad.bottom}" /> | |
| <line class="plot-axis" x1="${pad.left}" x2="${pad.left}" y1="${pad.top}" y2="${height - pad.bottom}" /> | |
| ${dateTicks.map((date) => `<text class="plot-label" x="${x(date)}" y="${height - 20}" text-anchor="middle">${dateLabel(date).slice(2, 7)}</text>`).join("")} | |
| ${yTicks.map((tick) => `<text class="plot-label" x="${pad.left - 10}" y="${y(tick) + 4}" text-anchor="end">${fmtValue(tick)}</text>`).join("")} | |
| <text class="plot-axis-title" x="${width / 2}" y="${height - 4}" text-anchor="middle">Date</text> | |
| <text class="plot-axis-title" x="-${height / 2}" y="14" transform="rotate(-90)" text-anchor="middle">K score</text> | |
| ${segments} | |
| ${points} | |
| </svg> | |
| </section> | |
| `; | |
| }).filter(Boolean).join(""); | |
| if (!cards) { | |
| target.innerHTML = '<p class="loading-note">No tier K progress data matches the selected filters.</p>'; | |
| return; | |
| } | |
| target.innerHTML = `${cards}<div class="plot-tooltip" hidden></div>`; | |
| const tooltip = target.querySelector(".plot-tooltip"); | |
| target.querySelectorAll(".metric-progress-point").forEach((point) => { | |
| point.addEventListener("mouseenter", () => { | |
| const rows = JSON.parse(point.dataset.tooltip); | |
| tooltip.innerHTML = `<table>${rows.map(([key, value]) => `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(value)}</td></tr>`).join("")}</table>`; | |
| tooltip.hidden = false; | |
| }); | |
| point.addEventListener("mousemove", (event) => { | |
| const rect = target.getBoundingClientRect(); | |
| const left = event.clientX - rect.left - tooltip.offsetWidth - 12; | |
| tooltip.style.left = `${Math.max(8, left)}px`; | |
| tooltip.style.top = `${event.clientY - rect.top + 12}px`; | |
| }); | |
| point.addEventListener("mouseleave", () => { | |
| tooltip.hidden = true; | |
| }); | |
| }); | |
| } | |
| function renderOverviewPlotPage() { | |
| renderOrganizationLegend(); | |
| renderOverviewPlot(sortedOverview()); | |
| renderMetricProgressPlots(); | |
| } | |
| function renderOverview() { | |
| const rows = sortedOverview(); | |
| const showModelInfo = state.showModelInfo; | |
| const best = Math.min(...rows.map((r) => r.overall).filter(finite)); | |
| const leader = [...(state.recordsReady ? tierStats() : (state.data?.overall || []))] | |
| .sort((a, b) => compareNullable(a.overall, b.overall) || a.model.localeCompare(b.model))[0]; | |
| const strip = $("winner-strip"); | |
| if (strip && leader) { | |
| strip.innerHTML = `<span>Overall #1</span><b>${displayName(leader.model)}</b><strong>Rank sum ${fmt(leader.overall, finite(leader.overall) && leader.overall % 1 ? 1 : 0)}</strong>`; | |
| } | |
| $("detail-rank-toggle").checked = state.showDetailRank; | |
| $("model-info-toggle").checked = state.showModelInfo; | |
| const hint = $("overview-mode-hint"); | |
| if (hint) { | |
| hint.innerHTML = `Overall rank is the sum of the <b>Point·Ranking·Decision</b> tier ranks. Each tier rank is ordered by the geometric mean K of metric ranks under the selected Detail Category and Frequency filters. Each cell is shown as <span class="cell-example"><span class="rankbadge hint-badge">A</span> <b>(B)</b></span>. <b>(B appears when the Detail Value toggle is on</b>). <b>A</b> is the tier rank, and <b>B</b> is the K score used to produce that rank. Lower <b>Rank sum</b> is better.`; | |
| } | |
| document.querySelector('#overview-table th[data-sort="pointwise"]').textContent = "Point rank"; | |
| document.querySelector('#overview-table th[data-sort="ic"]').textContent = "Ranking rank"; | |
| document.querySelector('#overview-table th[data-sort="portfolio"]').textContent = "Decision rank"; | |
| document.querySelector('#overview-table th[data-sort="overall"]').textContent = "Rank sum ↓"; | |
| document.querySelector('#overview-table th[data-sort="org"]').hidden = !showModelInfo; | |
| document.querySelector('#overview-table th[data-sort="model_size"]').hidden = !showModelInfo; | |
| document.querySelector('#overview-table th[data-sort="submit_time"]').hidden = !showModelInfo; | |
| const tierCell = (row, tier) => { | |
| const aspect = ASPECT[tier]; | |
| const rank = row[aspect]; | |
| const k = row[`${aspect}_k`]; | |
| if (!finite(k)) return `<td class="num">${fmt(k)}</td>`; | |
| const content = state.showDetailRank | |
| ? `${overviewRankBadge(rank)} <small class="cell-sub">(${overviewScoreText(k)})</small>` | |
| : overviewRankBadge(rank); | |
| return `<td class="num tier-rank" data-model="${row.model}" data-tier="${tier}">${content}</td>`; | |
| }; | |
| const overallCell = (row) => { | |
| const value = row.overall; | |
| const text = fmt(value, finite(value) && value % 1 ? 1 : 0); | |
| return `<td class="num rank-sum ${value === best ? "val-best" : ""}" data-model="${row.model}" title="View Point, Ranking, and Decision rank detail"><b>${text}</b></td>`; | |
| }; | |
| $("overview-body").innerHTML = rows.map((row, index) => ` | |
| <tr class="model-row ${isExaone(row.model) ? "exaone-row" : ""}"> | |
| <td class="num">${rankBadge(index + 1)}</td> | |
| ${renderModelTypeCell(row.model)} | |
| ${renderModelCell(row.model)} | |
| ${showModelInfo ? `<td>${modelOrg(row.model)}</td><td>${modelSize(row.model)}</td><td>${modelSubmitTime(row.model)}</td>` : ""} | |
| ${tierCell(row, "tier1")} | |
| ${tierCell(row, "tier2")} | |
| ${tierCell(row, "tier3")} | |
| ${overallCell(row)} | |
| </tr> | |
| `).join(""); | |
| $("overview-body").querySelectorAll(".tier-rank").forEach((td) => td.onclick = () => showTierDetail(td.dataset.model, td.dataset.tier)); | |
| $("overview-body").querySelectorAll(".rank-sum").forEach((td) => td.onclick = () => showModelDetail(td.dataset.model)); | |
| document.querySelectorAll("#overview-table th[data-sort]").forEach((th) => { | |
| th.classList.toggle("sorted", th.dataset.sort === state.sort.key); | |
| th.classList.toggle("asc", th.dataset.sort === state.sort.key && state.sort.asc); | |
| th.onclick = () => { | |
| if (state.sort.key === th.dataset.sort) state.sort.asc = !state.sort.asc; | |
| else { state.sort.key = th.dataset.sort; state.sort.asc = th.dataset.sort !== "model"; } | |
| renderOverview(); | |
| }; | |
| }); | |
| } | |
| function bindMetricsSortHeaders(columns) { | |
| const metricDirections = new Map(columns.map((column) => [`metric:${column.key}`, metricGroupDirection(column.metric)])); | |
| document.querySelectorAll("#metrics-head th[data-sort]").forEach((th) => { | |
| th.onclick = () => { | |
| const key = th.dataset.sort; | |
| if (state.metricsSort.key === key) { | |
| state.metricsSort.asc = !state.metricsSort.asc; | |
| } else { | |
| state.metricsSort.key = key; | |
| state.metricsSort.asc = key.startsWith("metric:") ? metricDirections.get(key) < 0 : true; | |
| } | |
| renderMetrics(); | |
| }; | |
| }); | |
| } | |
| function renderMetrics() { | |
| $("metrics-model-info-toggle").checked = state.showModelInfo; | |
| if (!state.recordsReady) { | |
| renderLoadingPanel("metrics-body"); | |
| return; | |
| } | |
| const scopedRecords = includedRecords(); | |
| const columns = metricGroupsByTier() | |
| .flatMap(([tier, metrics]) => metrics.map((metric) => ({ tier, metric, key: `${tier}|${metric}` }))) | |
| .filter((column) => !state.excludedMetrics.has(column.key) && scopedRecords.some((row) => metricFilterKey(row) === column.key)); | |
| const sortState = state.metricsSort; | |
| const sortClass = (key) => `${sortState.key === key ? " sorted" : ""}${sortState.key === key && sortState.asc ? " asc" : ""}`; | |
| const orgHead = state.showModelInfo | |
| ? `<th data-sort="org" class="${sortClass("org")}">Organization</th><th data-sort="model_size" class="${sortClass("model_size")}">Model size</th><th data-sort="submit_time" class="${sortClass("submit_time")}">Submit date</th>` | |
| : ""; | |
| const orgColspan = state.showModelInfo ? 5 : 2; | |
| if (!columns.length) { | |
| $("metrics-head").innerHTML = `<tr><th class="model-type-head${sortClass("model_type")}" data-sort="model_type" title="Model Type"></th><th data-sort="model" class="${sortClass("model")}">Model</th>${orgHead}</tr>`; | |
| $("metrics-body").innerHTML = `<tr><td class="skip" colspan="${orgColspan}">No metric is selected.</td></tr>`; | |
| bindMetricsSortHeaders(columns); | |
| return; | |
| } | |
| const rows = tierStats(); | |
| const valuesByModel = new Map(); | |
| for (const model of includedModels()) { | |
| const map = new Map(); | |
| for (const column of columns) { | |
| map.set(column.key, arithmeticMean(scopedRecords | |
| .filter((row) => row.model === model && metricFilterKey(row) === column.key) | |
| .map((row) => row.value))); | |
| } | |
| valuesByModel.set(model, map); | |
| } | |
| const bestByColumn = new Map(columns.map((column) => { | |
| const values = rows.map((row) => valuesByModel.get(row.model).get(column.key)).filter(finite); | |
| const direction = metricGroupDirection(column.metric); | |
| return [column.key, values.length ? (direction > 0 ? Math.max(...values) : Math.min(...values)) : null]; | |
| })); | |
| const valueForSort = (row, key) => { | |
| if (key === "rank") return row.overall; | |
| if (key === "model_type") return modelTypeLabel(modelType(row.model)); | |
| if (key === "model") return displayName(row.model); | |
| if (key === "org") return modelOrg(row.model); | |
| if (key === "model_size") return parseModelSize(modelSize(row.model)); | |
| if (key === "submit_time") return modelSubmitTime(row.model); | |
| if (key.startsWith("metric:")) return valuesByModel.get(row.model)?.get(key.slice(7)); | |
| return row[key]; | |
| }; | |
| rows.sort((a, b) => { | |
| const key = sortState.key; | |
| const av = valueForSort(a, key); | |
| const bv = valueForSort(b, key); | |
| if (["model_type", "model", "org", "submit_time"].includes(key)) { | |
| const cmp = String(av || "").localeCompare(String(bv || "")); | |
| return sortState.asc ? cmp : -cmp; | |
| } | |
| return compareNullable(av, bv, sortState.asc) || displayName(a.model).localeCompare(displayName(b.model)); | |
| }); | |
| $("metrics-head").innerHTML = `<tr><th class="num${sortClass("rank")}" data-sort="rank">#</th><th class="model-type-head${sortClass("model_type")}" data-sort="model_type" title="Model Type"></th><th data-sort="model" class="${sortClass("model")}">Model</th>${orgHead}${columns.map((column) => `<th class="num${sortClass(`metric:${column.key}`)}" data-sort="metric:${column.key}" title="${TIER_SHORT[column.tier]} · ${column.metric}">${TIER_SHORT[column.tier]}<br>${metricGroupLabel(column.metric)}</th>`).join("")}</tr>`; | |
| $("metrics-body").innerHTML = rows.map((row, index) => { | |
| const modelValues = valuesByModel.get(row.model); | |
| return `<tr class="${isExaone(row.model) ? "exaone-row" : ""}"> | |
| <td class="num">${rankBadge(index + 1)}</td> | |
| ${renderModelTypeCell(row.model)} | |
| ${renderModelCell(row.model)} | |
| ${state.showModelInfo ? `<td>${modelOrg(row.model)}</td><td>${modelSize(row.model)}</td><td>${modelSubmitTime(row.model)}</td>` : ""} | |
| ${columns.map((column) => { | |
| const value = modelValues.get(column.key); | |
| return finite(value) | |
| ? `<td class="num metric-score ${value === bestByColumn.get(column.key) ? "val-best" : ""}" data-model="${row.model}" data-tier="${column.tier}" data-metric-group="${column.metric}" title="View average components">${fmtValue(value)}</td>` | |
| : `<td class="num">${fmtValue(value)}</td>`; | |
| }).join("")} | |
| </tr>`; | |
| }).join(""); | |
| $("metrics-body").querySelectorAll(".metric-score").forEach((td) => { | |
| td.onclick = () => showMetricScoreDetail(td.dataset.model, td.dataset.tier, td.dataset.metricGroup); | |
| }); | |
| bindMetricsSortHeaders(columns); | |
| } | |
| function showMetricScoreDetail(model, tier, metricGroup) { | |
| const rows = includedRecords() | |
| .filter((row) => row.model === model && metricFilterKey(row) === `${tier}|${metricGroup}`) | |
| .sort((a, b) => a.category.localeCompare(b.category) | |
| || a.detail_category.localeCompare(b.detail_category) | |
| || a.frequency.localeCompare(b.frequency) | |
| || metricBase(a.metric).localeCompare(metricBase(b.metric)) | |
| || a.metric.localeCompare(b.metric)); | |
| const mean = (values) => { | |
| const valid = values.filter(finite); | |
| return valid.length ? valid.reduce((sum, value) => sum + value, 0) / valid.length : null; | |
| }; | |
| const overall = mean(rows.map((row) => row.value)); | |
| let html = `<p class="detail-k">${TIER_SHORT[tier]} · ${metricGroupLabel(metricGroup)} average = <b>${fmtValue(overall)}</b> · ${rows.length.toLocaleString()} cells</p>`; | |
| html += `<div class="period-card"><h3>Description for Date Codes</h3><div class="period-help"><span><b>D</b> Day</span><span><b>W</b> Week</span><span><b>M</b> Month</span><span><b>Q</b> Quarter</span><span><b>H</b> Half-year</span><span><b>Y</b> Year</span><span><b>B</b> Business</span><span><b>BD</b> Business-day</span></div></div>`; | |
| html += `<table><thead><tr><th>Detail Category</th><th>Freq</th><th>Metrics</th><th>Target Type</th><th>Pred. Target</th><th class="num">Value</th><th class="num">Rank</th></tr></thead><tbody>`; | |
| for (const row of rows) { | |
| const parts = parseMetricParts(row.metric); | |
| const direction = metricDirection(row.metric); | |
| const total = state.records.filter((candidate) => { | |
| return candidate.tier === row.tier | |
| && candidate.detail_category === row.detail_category | |
| && candidate.frequency === row.frequency | |
| && candidate.series_id === row.series_id | |
| && candidate.metric === row.metric; | |
| }).length; | |
| html += `<tr><td title="${row.category || ""}">${displayDetailLabel(row.detail_category)}</td><td title="${row.frequency}">${freqCode(row.frequency)}</td><td title="${row.metric}">${parts.name} <span class="dir-arrow ${direction > 0 ? "up" : "down"}">${direction > 0 ? "↑" : "↓"}</span></td><td>${parts.type}</td><td>${parts.target}</td><td class="num">${fmtValue(row.value)}</td><td class="num">${rankText(row.rank, total)}</td></tr>`; | |
| } | |
| html += `</tbody></table><p class="note">These are the raw metric values and ranks used to form this average cell under the current left-side Metric · Detail Category · Frequency filters.</p>`; | |
| openDrawer(`${displayName(model)} · ${metricGroupLabel(metricGroup)}`, html); | |
| closeDrawer2(); | |
| } | |
| function unitK(tier, records = includedRecords()) { | |
| const out = new Map(); | |
| for (const model of includedModels()) out.set(model, new Map()); | |
| const grouped = new Map(); | |
| for (const row of records.filter((r) => !tier || r.tier === tier)) { | |
| const key = JSON.stringify([row.model, row.unit]); | |
| if (!grouped.has(key)) grouped.set(key, []); | |
| grouped.get(key).push(row.rank); | |
| } | |
| for (const [key, ranks] of grouped) { | |
| const [model, unit] = JSON.parse(key); | |
| if (out.has(model)) out.get(model).set(unit, geometricMean(ranks)); | |
| } | |
| return out; | |
| } | |
| function renderStrength() { | |
| if (!state.recordsReady) { | |
| $("strength-body").innerHTML = '<p class="loading-note">Loading detailed records.</p>'; | |
| return; | |
| } | |
| const scopedRecords = includedRecords(); | |
| if (!scopedRecords.length) { | |
| $("strength-body").innerHTML = '<p class="loading-note">No cells match the selected filters.</p>'; | |
| return; | |
| } | |
| const models = includedModels(); | |
| const overallScores = new Map(models.map((model) => [ | |
| model, | |
| geometricMean(scopedRecords.filter((row) => row.model === model).map((row) => row.rank)) | |
| ])); | |
| const overallRanks = new Map(rankRows([...overallScores.entries()].map(([model, value]) => ({ model, value })), "value", -1).map((row) => [row.model, row.rank])); | |
| const units = [...new Map(scopedRecords.map((r) => [r.unit, displayDetailLabel(r.unit_label || r.unit)])).entries()].sort((a, b) => a[1].localeCompare(b[1])); | |
| const matrix = unitK(null, scopedRecords); | |
| const overall = models | |
| .filter((model) => finite(overallScores.get(model))) | |
| .sort((a, b) => compareNullable(overallScores.get(a), overallScores.get(b)) || a.localeCompare(b)); | |
| const colRanks = new Map(); | |
| for (const [unit] of units) { | |
| const vals = models.map((model) => ({ model, value: matrix.get(model).get(unit) })); | |
| colRanks.set(unit, new Map(rankRows(vals, "value", -1).map((r) => [r.model, r.rank]))); | |
| } | |
| const color = (rank, n) => { | |
| if (!finite(rank)) return "#f3f4f6"; | |
| const t = n > 1 ? (rank - 1) / (n - 1) : 0; | |
| return `hsl(${120 * (1 - Math.max(0, Math.min(1, t)))}, 62%, 78%)`; | |
| }; | |
| let html = `<table class="smap"><thead><tr><th class="mh">Model (${overall.length})</th><th class="ov">Overall</th>`; | |
| for (const [, label] of units) html += `<th class="sp">${strengthHeaderLabel(label)}</th>`; | |
| html += `</tr></thead><tbody>`; | |
| for (const model of overall) { | |
| const ov = overallScores.get(model); | |
| const ovRank = overallRanks.get(model); | |
| html += `<tr class="${isExaone(model) ? "exaone-row" : ""}"><td class="mh">${displayName(model)}</td><td class="ov" style="background:${color(ovRank, overall.length)}" title="Overall K geomean">${fmt(ov)}</td>`; | |
| for (const [unit] of units) { | |
| const k = matrix.get(model).get(unit); | |
| const rank = colRanks.get(unit).get(model); | |
| html += `<td style="background:${color(rank, overall.length)}" title="Column rank ${finite(rank) ? rank : "—"}">${finite(k) ? k.toFixed(1) : "·"}</td>`; | |
| } | |
| html += `</tr>`; | |
| } | |
| $("strength-body").innerHTML = html + `</tbody></table>`; | |
| } | |
| function guideTable(title, rows) { | |
| return ` | |
| <section class="guide-card"> | |
| <h3>${title}</h3> | |
| <table class="board guide-table"><thead><tr><th>Button</th><th>Meaning</th></tr></thead><tbody> | |
| ${rows.map(([button, meaning]) => `<tr><td class="model-name">${button}</td><td>${meaning}</td></tr>`).join("")} | |
| </tbody></table> | |
| </section> | |
| `; | |
| } | |
| function renderLeaderboardGuide() { | |
| if (!state.recordsReady) { | |
| $("leaderboard-guide-body").innerHTML = '<p class="loading-note">Loading detailed records.</p>'; | |
| return; | |
| } | |
| const metricRows = metricGroupsByTier().flatMap(([tier, metrics]) => metrics.map((metric) => { | |
| const base = metric === "mase_pct0" ? "mase" : metric; | |
| const desc = metric === "mase_pct0" ? "Forecast error on the original target without a percentage-change option." : (METRIC_INFO[base]?.[1] || "Includes this metric group in rank calculations."); | |
| return [`${TIER_SHORT[tier]} · ${metricGroupLabel(metric)}`, desc]; | |
| })); | |
| const optionGroups = metricOptionGroups(); | |
| const typeRows = optionButtonGroups(optionGroups.types, typeLabel, true).map((group) => { | |
| const descriptions = { | |
| All: "Original-target metrics without a specific percentage-change type.", | |
| WoW: "Week-over-week percentage change as the target type.", | |
| MoM: "Month-over-month percentage change as the target type.", | |
| QoQ: "Quarter-over-quarter percentage change as the target type.", | |
| HoH: "Half-year-over-half-year percentage change as the target type.", | |
| YoY: "Year-over-year percentage change as the target type." | |
| }; | |
| return [group.label, descriptions[group.label] || "Includes this target type in rank calculations."]; | |
| }); | |
| const targetRows = optionButtonGroups(optionGroups.targets, predTargetLabel).map((group) => { | |
| const descriptions = { | |
| Original: "Original target without a forecast horizon label.", | |
| "1W": "Forecast target one week ahead.", | |
| "1M": "Forecast target one month ahead.", | |
| "1Q": "Forecast target one quarter ahead.", | |
| "1H": "Forecast target one half-year ahead.", | |
| "1Y": "Forecast target one year ahead." | |
| }; | |
| return [group.label, descriptions[group.label] || "Includes this prediction target horizon in rank calculations."]; | |
| }); | |
| const detailRows = detailsByScope().flatMap(([, values]) => [...values.entries()] | |
| .sort((a, b) => a[1].localeCompare(b[1])) | |
| .map(([detail, label]) => [label, DETAIL_CATEGORY_EXPLANATIONS[detail] || DETAIL_CATEGORY_EXPLANATIONS[label] || "Evaluation data for this Detail Category."])); | |
| const frequencyRows = allFrequencies().map((frequency) => { | |
| const descriptions = { | |
| Annual: "Time series observed at an annual frequency.", | |
| "Business-daily": "Time series observed at a business-daily frequency.", | |
| Daily: "Time series observed at a daily frequency.", | |
| Monthly: "Time series observed at a monthly frequency.", | |
| Quarterly: "Time series observed at a quarterly frequency.", | |
| Weekly: "Time series observed at a weekly frequency." | |
| }; | |
| return [frequency, descriptions[frequency] || `Time series observed at a ${frequency} frequency.`]; | |
| }); | |
| $("leaderboard-guide-body").innerHTML = ` | |
| <div class="guide-grid"> | |
| ${guideTable("Metric", metricRows)} | |
| ${guideTable("Metric Additional Option · Target Type", typeRows)} | |
| ${guideTable("Metric Additional Option · Pred. Target", targetRows)} | |
| ${guideTable("Detail Category", detailRows)} | |
| ${guideTable("Frequency", frequencyRows)} | |
| </div> | |
| `; | |
| } | |
| function showModelDetail(model) { | |
| if (!state.recordsReady) return; | |
| const stats = tierStats().find((r) => r.model === model); | |
| const units = [...new Map(includedRecords().filter((r) => r.model === model).map((r) => [r.unit, displayDetailLabel(r.unit_label || r.unit)])).entries()].sort((a, b) => a[1].localeCompare(b[1])); | |
| const categoryByUnit = new Map(includedRecords().filter((r) => r.model === model).map((r) => [r.unit, r.category || ""])); | |
| const matrixByTier = Object.fromEntries(Object.keys(TIER_LABEL).map((tier) => [tier, unitK(tier).get(model)])); | |
| let html = `<p class="detail-k">Rank sum = <b>${fmt(stats.overall, finite(stats.overall) && stats.overall % 1 ? 1 : 0)}</b></p><table><thead><tr><th>Category</th><th>Detail Category</th><th class="num">Point</th><th class="num">Ranking</th><th class="num">Decision</th></tr></thead><tbody>`; | |
| const cell = (tier, unit) => { | |
| const value = matrixByTier[tier].get(unit); | |
| return finite(value) | |
| ? `<td class="num cat-rank" data-model="${model}" data-tier="${tier}" data-unit="${unit}" title="View ${TIER_SHORT[tier]} raw metric values">${fmt(value)}</td>` | |
| : `<td class="num">${fmt(value)}</td>`; | |
| }; | |
| for (const [unit, label] of units) html += `<tr><td>${categoryByUnit.get(unit) || ""}</td><td title="${unit}">${label}</td>${cell("tier1", unit)}${cell("tier2", unit)}${cell("tier3", unit)}</tr>`; | |
| html += `</tbody></table><p class="note">Each cell is the geometric mean K of metric ranks for that Detail Category under the selected Frequency filters. Lower is better. <b>Click a number</b> to view raw metric values and ranks.</p>`; | |
| openDrawer(displayName(model), html); | |
| closeDrawer2(); | |
| $("detail-body").querySelectorAll(".cat-rank").forEach((td) => { | |
| td.onclick = () => showMetricDetail(td.dataset.model, td.dataset.tier, td.dataset.unit); | |
| }); | |
| } | |
| function showTierDetail(model, tier) { | |
| if (!state.recordsReady) return; | |
| const matrix = unitK(tier).get(model); | |
| const units = [...new Map(includedRecords().filter((r) => r.model === model && r.tier === tier).map((r) => [r.unit, displayDetailLabel(r.unit_label || r.unit)])).entries()].sort((a, b) => a[1].localeCompare(b[1])); | |
| const categoryByUnit = new Map(includedRecords().filter((r) => r.model === model && r.tier === tier).map((r) => [r.unit, r.category || ""])); | |
| let html = `<p class="detail-k">${TIER_SHORT[tier]} K = <b>${fmt(tierStats().find((r) => r.model === model)[`${ASPECT[tier]}_k`])}</b></p><table><thead><tr><th>Category</th><th>Detail Category</th><th class="num">K</th></tr></thead><tbody>`; | |
| for (const [unit, label] of units) { | |
| const value = matrix.get(unit); | |
| html += `<tr><td>${categoryByUnit.get(unit) || ""}</td><td title="${unit}">${label}</td>${finite(value) | |
| ? `<td class="num cat-rank" data-model="${model}" data-tier="${tier}" data-unit="${unit}" title="View raw metric values">${fmt(value)}</td>` | |
| : `<td class="num">${fmt(value)}</td>`}</tr>`; | |
| } | |
| html += `</tbody></table><p class="note"><b>Click a K value</b> to view the raw metric values and ranks used in the calculation.</p>`; | |
| openDrawer(`${displayName(model)} · ${TIER_SHORT[tier]}`, html); | |
| closeDrawer2(); | |
| $("detail-body").querySelectorAll(".cat-rank").forEach((td) => { | |
| td.onclick = () => showMetricDetail(td.dataset.model, td.dataset.tier, td.dataset.unit); | |
| }); | |
| } | |
| function showMetricDetail(model, tier, unit) { | |
| if (!state.recordsReady) return; | |
| const rows = includedRecords() | |
| .filter((r) => r.model === model && r.tier === tier && r.unit === unit) | |
| .sort((a, b) => { | |
| const [aType, aTarget] = a.metric.replace(new RegExp(`^${metricBase(a.metric)}_?`), "").split("_"); | |
| const [bType, bTarget] = b.metric.replace(new RegExp(`^${metricBase(b.metric)}_?`), "").split("_"); | |
| return a.frequency.localeCompare(b.frequency) | |
| || metricBase(a.metric).localeCompare(metricBase(b.metric)) | |
| || horizonOrder(aType) - horizonOrder(bType) | |
| || horizonOrder(aTarget) - horizonOrder(bTarget) | |
| || a.metric.localeCompare(b.metric); | |
| }); | |
| const label = displayDetailLabel(rows[0]?.unit_label || unit); | |
| const k = geometricMean(rows.map((r) => r.rank)); | |
| let html = `<p class="detail-k">${TIER_SHORT[tier]} · ${label} K = <b>${fmt(k)}</b> · ${rows.length.toLocaleString()} cells</p>`; | |
| html += `<div class="period-card"><h3>Description for Date Codes</h3><div class="period-help"><span><b>D</b> Day</span><span><b>W</b> Week</span><span><b>M</b> Month</span><span><b>Q</b> Quarter</span><span><b>H</b> Half-year</span><span><b>Y</b> Year</span><span><b>B</b> Business</span><span><b>BD</b> Business-day</span></div></div>`; | |
| html += `<table><thead><tr><th>Freq</th><th>Metrics</th><th>Target Type</th><th>Pred. Target</th><th class="num">Value</th><th class="num">Rank</th></tr></thead><tbody>`; | |
| for (const row of rows) { | |
| const parts = parseMetricParts(row.metric); | |
| const direction = metricDirection(row.metric); | |
| const total = state.records.filter((candidate) => { | |
| return candidate.tier === row.tier | |
| && candidate.detail_category === row.detail_category | |
| && candidate.frequency === row.frequency | |
| && candidate.series_id === row.series_id | |
| && candidate.metric === row.metric; | |
| }).length; | |
| html += `<tr><td title="${row.frequency}">${freqCode(row.frequency)}</td><td title="${row.metric}">${parts.name} <span class="dir-arrow ${direction > 0 ? "up" : "down"}">${direction > 0 ? "↑" : "↓"}</span></td><td>${parts.type}</td><td>${parts.target}</td><td class="num">${fmtValue(row.value)}</td><td class="num">${rankText(row.rank, total)}</td></tr>`; | |
| } | |
| html += `</tbody></table><p class="note">↑ means higher is better, and ↓ means lower is better. Rank is shown as rank/total within the same tier · Detail Category · Frequency · metric comparison unit.</p>`; | |
| $("detail2-title").textContent = `${label} · ${TIER_SHORT[tier]}`; | |
| $("detail2-body").innerHTML = html; | |
| $("detail2").hidden = false; | |
| $("detail").classList.add("shifted"); | |
| $("drawer-scrim").hidden = false; | |
| } | |
| function openDrawer(title, html) { | |
| $("detail-title").textContent = title; | |
| $("detail-body").innerHTML = html; | |
| $("detail").classList.remove("shifted"); | |
| $("detail").hidden = false; | |
| $("drawer-scrim").hidden = false; | |
| } | |
| function closeDrawer2() { | |
| const drawer = $("detail2"); | |
| if (!drawer) return; | |
| drawer.hidden = true; | |
| $("detail").classList.remove("shifted"); | |
| } | |
| function closeDrawer() { | |
| closeDrawer2(); | |
| $("detail").hidden = true; | |
| $("drawer-scrim").hidden = true; | |
| } | |
| function render() { | |
| syncSidebarForView(); | |
| if (state.view === "overview") renderOverview(); | |
| if (state.view === "overview-plot") renderOverviewPlotPage(); | |
| if (state.view === "metrics") renderMetrics(); | |
| if (state.view === "strength") renderStrength(); | |
| if (state.view === "leaderboard-guide") renderLeaderboardGuide(); | |
| } | |
| async function boot() { | |
| state.data = await fetch("leaderboard_data.json", { cache: "no-store" }).then((response) => response.json()); | |
| state.models = (state.data.overall || []).map((row) => row.model).sort(); | |
| const configEntries = await Promise.all((state.data.result_files || []).map((item) => { | |
| const path = item.config_path || item.path.replace(/result\.json$/, "config.json"); | |
| return fetch(path, { cache: "no-store" }) | |
| .then((response) => response.ok ? response.json() : null) | |
| .then((config) => [item.model, config || { model: item.model, model_type: "unknown" }]); | |
| })); | |
| state.modelConfigs = new Map(configEntries); | |
| $("model-count").textContent = `${state.models.length} models`; | |
| $("updated").textContent = `updated ${formatAoeDate(state.data.generated_at)}`; | |
| $("detail-close").onclick = closeDrawer; | |
| $("detail2-close").onclick = closeDrawer2; | |
| $("drawer-scrim").onclick = closeDrawer; | |
| $("detail-rank-toggle").onchange = (event) => { | |
| state.showDetailRank = event.target.checked; | |
| renderOverview(); | |
| }; | |
| $("model-info-toggle").onchange = (event) => { | |
| state.showModelInfo = event.target.checked; | |
| render(); | |
| }; | |
| $("metrics-model-info-toggle").onchange = (event) => { | |
| state.showModelInfo = event.target.checked; | |
| render(); | |
| }; | |
| setupTabs(); | |
| setupCollapsibles(); | |
| buildSpecToggles(); | |
| render(); | |
| const columns = state.data.rank_record_columns || []; | |
| let chunks = []; | |
| if (state.data.result_files?.length) { | |
| const results = await Promise.all(state.data.result_files.map((item) => fetch(item.path, { cache: "no-store" }).then((response) => response.json()))); | |
| chunks = results.flatMap((result) => result.records || []); | |
| } else if (state.data.result_index) { | |
| const index = await fetch(state.data.result_index, { cache: "no-store" }).then((response) => response.json()); | |
| const results = await Promise.all(index.models.map((item) => fetch(item.path, { cache: "no-store" }).then((response) => response.json()))); | |
| chunks = results.flatMap((result) => result.records || []); | |
| } else { | |
| chunks = state.data.rank_record_files | |
| ? (await Promise.all(state.data.rank_record_files.map((file) => fetch(file, { cache: "no-store" }).then((response) => response.json())))).flat() | |
| : (state.data.rank_records || []); | |
| } | |
| state.records = chunks.map((row) => Array.isArray(row) | |
| ? Object.fromEntries(columns.map((column, index) => [column, row[index]])) | |
| : row); | |
| state.models = [...new Set(state.records.map((r) => r.model))].sort(); | |
| state.recordsReady = true; | |
| $("model-count").textContent = `${state.models.length} models`; | |
| buildSpecToggles(); | |
| render(); | |
| } | |
| boot(); | |