Align leaderboard with the InferenceNet Challenge website
Browse filesBring results into the first screen with the Challenge website’s light/dark styling, a five-metric table and the shared mountain visualization. Add an all-results view while preserving paired-only comparisons, DSH provenance and expandable scoring evidence.
Validated all 13 runs, five sorting metrics, 6 baseline / 7 harness / 6 paired views, search, Chinese/English, light/dark themes and narrow layouts. Published data, scoring and result archives are unchanged.
- index.html +40 -22
- leaderboard.css +17 -7
- leaderboard.js +110 -29
- mountain.css +145 -0
- mountain.js +499 -0
index.html
CHANGED
|
@@ -4,30 +4,33 @@
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<meta name="description" content="InferenceNet research leaderboard: single-pass generation, DeepAgents and an independent DeepSeek Harness result on 1,000 econometric replication tasks.">
|
| 7 |
-
<meta name="color-scheme" content="dark">
|
| 8 |
-
<title>
|
| 9 |
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c1219'/%3E%3Ctext x='5' y='23' font-family='sans-serif' font-size='21' fill='%2378dbc0'%3EiN%3C/text%3E%3C/svg%3E">
|
| 10 |
<link rel="stylesheet" href="./leaderboard.css">
|
| 11 |
-
<
|
|
|
|
| 12 |
</head>
|
| 13 |
<body>
|
| 14 |
<a class="skip" href="#rankings">Skip to rankings</a>
|
| 15 |
<main class="page">
|
| 16 |
<nav class="topbar" aria-label="Main navigation">
|
| 17 |
-
<a class="brand" href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer">
|
| 18 |
<div class="nav-links">
|
|
|
|
| 19 |
<a href="https://huggingface.co/datasets/CamoAiLab/InferenceNet" target="_blank" rel="noopener noreferrer" data-i18n="dataset">Dataset ↗</a>
|
| 20 |
<a href="https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/tree/main/results" target="_blank" rel="noopener noreferrer" data-i18n="archive">Results archive ↗</a>
|
| 21 |
<button id="language" type="button" aria-label="Switch to Chinese">中文</button>
|
|
|
|
| 22 |
</div>
|
| 23 |
</nav>
|
| 24 |
|
| 25 |
<header class="hero">
|
| 26 |
-
<p class="eyebrow"><span
|
| 27 |
-
<h1 data-i18n="title">
|
| 28 |
-
<p class="intro" data-i18n="intro">
|
| 29 |
<div class="facts" aria-label="Dataset coverage">
|
| 30 |
-
<span><strong id="model-count">—</strong><span data-i18n="models">
|
| 31 |
<span><strong id="group-count">—</strong><span data-i18n="groups">experiment groups</span></span>
|
| 32 |
<span><strong id="task-count">—</strong><span data-i18n="tasks">tasks per group</span></span>
|
| 33 |
<span><strong id="record-count">—</strong><span data-i18n="records">task records</span></span>
|
|
@@ -35,27 +38,23 @@
|
|
| 35 |
</div>
|
| 36 |
</header>
|
| 37 |
|
| 38 |
-
<section class="protocols" aria-label="Experimental settings">
|
| 39 |
-
<div><p class="protocol-tag baseline"><span aria-hidden="true">A</span><strong data-i18n="single">Single-pass Agent</strong></p><p data-i18n="singleDesc">One code-generation call, followed by execution. No iterative agent loop.</p></div>
|
| 40 |
-
<div><p class="protocol-tag harness"><span aria-hidden="true">B</span><strong data-i18n="deepagents">DeepAgents</strong></p><p data-i18n="harnessDesc">DeepAgents plans, uses tools and revises code. Up to 6 model calls, plus final execution.</p></div>
|
| 41 |
-
<div><p class="protocol-tag independent"><span aria-hidden="true">C</span><strong data-i18n="dsh">DeepSeek Harness · separate run</strong></p><p data-i18n="dshDesc">Official DSH SDK, requested v4-pro API alias. A fixed technical recovery selection; underlying model weights are unverified.</p></div>
|
| 42 |
-
</section>
|
| 43 |
|
| 44 |
<section id="rankings" aria-labelledby="rankings-title">
|
| 45 |
-
<div class="section-head"><div><p class="eyebrow" data-i18n="resultsLabel">01 / RESULTS</p><h2 id="rankings-title" data-i18n="rankings">Agent & Harness leaderboard</h2></div><span class="badge" data-i18n="research">Research results · provisional</span></div>
|
| 46 |
-
|
| 47 |
-
|
| 48 |
|
| 49 |
<div class="toolbar">
|
| 50 |
<div class="view-switch" role="group" aria-label="Leaderboard view">
|
|
|
|
| 51 |
<button type="button" data-view="compare" aria-pressed="false" data-i18n="compare">Baseline / DeepAgents pairs</button>
|
| 52 |
<button type="button" data-view="baseline" aria-pressed="false" data-i18n="single">Single-pass Agent</button>
|
| 53 |
-
<button type="button" data-view="harness" aria-pressed="
|
| 54 |
</div>
|
| 55 |
<a class="download" href="./leaderboard.csv" download data-i18n="download">Download CSV ↓</a>
|
| 56 |
</div>
|
| 57 |
<div class="filters">
|
| 58 |
-
<label class="metric-control"><span data-i18n="metric">
|
| 59 |
<optgroup label="Local paper profile" data-group="local">
|
| 60 |
<option value="perfect" data-i18n="perfect">Full replication · local</option>
|
| 61 |
</optgroup>
|
|
@@ -69,7 +68,15 @@
|
|
| 69 |
<label id="sort-control"><span data-i18n="sort">Sort by</span><select id="sort"><option value="deepagents" data-i18n="harnessScore">DeepAgents score</option><option value="baseline" data-i18n="singleScore">Single-pass score</option><option value="gain" data-i18n="lift">Lift</option></select></label>
|
| 70 |
<label class="search-control"><span data-i18n="search">Find a model</span><input id="search" type="search" placeholder="Search models…" autocomplete="off"></label>
|
| 71 |
</div>
|
| 72 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
<p id="load-state" role="status" class="load-state">Loading archived results…</p>
|
| 74 |
<div id="error" class="error" role="alert" hidden><p data-i18n="error">Results could not be loaded. Please retry or open the results archive.</p><button type="button" id="retry" data-i18n="retry">Retry</button></div>
|
| 75 |
<div id="table-wrap" class="table-wrap" tabindex="0" aria-label="Scrollable leaderboard" hidden>
|
|
@@ -77,22 +84,33 @@
|
|
| 77 |
</div>
|
| 78 |
<p id="empty" class="load-state" hidden data-i18n="empty">No matching models.</p>
|
| 79 |
<div class="table-foot"><span id="result-count" aria-live="polite"></span><span data-i18n="denominator">Every score uses all 1,000 tasks. Unknown and invalid outcomes earn no successes.</span></div>
|
|
|
|
| 80 |
</section>
|
| 81 |
|
| 82 |
-
<section class="method" aria-
|
| 83 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
<div class="method-grid">
|
| 85 |
<article><h3 data-i18n="fixedTasks">Fixed task set</h3><p data-i18n="fixedDesc">The same Selected_1000 task IDs and dataset revision are used for every group. Each score is successes ÷ 1,000; failed or unknown tasks stay in the denominator.</p></article>
|
| 86 |
<article><h3 data-i18n="separateMetrics">Two scoring profiles</h3><p data-i18n="profilesDesc">Full replication is the local paper metric. The four HF-style metrics reproduce the published descriptions locally; they are not yet verified against the official scorer.</p></article>
|
| 87 |
<article><h3 data-i18n="budgetTitle">Read lift with the budgets</h3><p data-i18n="budgetDesc">Baseline: 1 model call. DeepAgents and DSH: up to 6 model calls and 4 trial tool calls per attempt. DSH adds 87 recovery attempts; full environment and reasoning parity is unverified. DSH has no paired lift; these are not equal-cost causal estimates.</p></article>
|
| 88 |
</div>
|
| 89 |
-
|
| 90 |
<p data-i18n="unknownDesc">“Metric unknown” means the selected metric cannot be assessed, including failed execution or unavailable outputs. It is different from a task whose final status is unknown. Neither is dropped or counted as a success.</p>
|
| 91 |
<ul id="definitions"></ul>
|
| 92 |
<div class="evidence-table-wrap"><table class="evidence-table"><caption data-i18n="evidenceCaption">Status from each accepted archive</caption><thead><tr><th data-i18n="model">Model / API alias</th><th data-i18n="protocol">Protocol</th><th data-i18n="evidenceCol">Evidence</th></tr></thead><tbody id="evidence-body"></tbody></table></div>
|
| 93 |
<p data-i18n="verification">HF-style counts are checked against archived task flags. Full-replication counts for Sol and Opus come from accepted summaries; all other displayed runs, including DSH, have per-task local flags. This refresh does not rerun or rescore any experiment.</p>
|
| 94 |
<p data-i18n="archiveNote">Per-run archive files remain frozen, including their original publication metadata. This page is a new display derived from those archives. GPT-5.5 and unarchived experiments are not included in this edition.</p>
|
|
|
|
| 95 |
</details>
|
|
|
|
| 96 |
</section>
|
| 97 |
<footer><span>InferenceNet <span class="muted">/</span> <span data-i18n="footer">Econometric replication, measured.</span></span><div><a id="revision-link" href="https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/tree/main/results" target="_blank" rel="noopener noreferrer" data-i18n="source">Source snapshot ↗</a><a href="./leaderboard-data.json" data-i18n="json">Data JSON ↗</a><a href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer" data-i18n="project">Project ↗</a></div></footer>
|
| 98 |
</main>
|
|
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<meta name="description" content="InferenceNet research leaderboard: single-pass generation, DeepAgents and an independent DeepSeek Harness result on 1,000 econometric replication tasks.">
|
| 7 |
+
<meta name="color-scheme" content="light dark">
|
| 8 |
+
<title>Leaderboard — InferenceNet Challenge</title>
|
| 9 |
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c1219'/%3E%3Ctext x='5' y='23' font-family='sans-serif' font-size='21' fill='%2378dbc0'%3EiN%3C/text%3E%3C/svg%3E">
|
| 10 |
<link rel="stylesheet" href="./leaderboard.css">
|
| 11 |
+
<link rel="stylesheet" href="./mountain.css">
|
| 12 |
+
<script type="module" src="./leaderboard.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
<a class="skip" href="#rankings">Skip to rankings</a>
|
| 16 |
<main class="page">
|
| 17 |
<nav class="topbar" aria-label="Main navigation">
|
| 18 |
+
<a class="brand" href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer">InferenceNet <small>CHALLENGE</small></a>
|
| 19 |
<div class="nav-links">
|
| 20 |
+
<a href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer" data-i18n="website">Project website ↗</a>
|
| 21 |
<a href="https://huggingface.co/datasets/CamoAiLab/InferenceNet" target="_blank" rel="noopener noreferrer" data-i18n="dataset">Dataset ↗</a>
|
| 22 |
<a href="https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/tree/main/results" target="_blank" rel="noopener noreferrer" data-i18n="archive">Results archive ↗</a>
|
| 23 |
<button id="language" type="button" aria-label="Switch to Chinese">中文</button>
|
| 24 |
+
<button id="theme" type="button" aria-pressed="false"><span class="theme-icon" aria-hidden="true"></span><span id="theme-label">Light</span></button>
|
| 25 |
</div>
|
| 26 |
</nav>
|
| 27 |
|
| 28 |
<header class="hero">
|
| 29 |
+
<p class="eyebrow"><span data-i18n="edition">LEADERBOARD · SEPTEMBER 2026</span></p>
|
| 30 |
+
<h1 data-i18n="title">InferenceNet Challenge Leaderboard</h1>
|
| 31 |
+
<p class="intro" data-i18n="intro">Models and harnesses on 1,000 econometric replication tasks.</p>
|
| 32 |
<div class="facts" aria-label="Dataset coverage">
|
| 33 |
+
<span><strong id="model-count">—</strong><span data-i18n="models">model aliases</span></span>
|
| 34 |
<span><strong id="group-count">—</strong><span data-i18n="groups">experiment groups</span></span>
|
| 35 |
<span><strong id="task-count">—</strong><span data-i18n="tasks">tasks per group</span></span>
|
| 36 |
<span><strong id="record-count">—</strong><span data-i18n="records">task records</span></span>
|
|
|
|
| 38 |
</div>
|
| 39 |
</header>
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
<section id="rankings" aria-labelledby="rankings-title">
|
| 43 |
+
<div class="section-head"><div><p class="eyebrow" data-i18n="resultsLabel">01 / RESULTS</p><h2 id="rankings-title" class="sr-only" data-i18n="rankings">Agent & Harness leaderboard</h2></div><span class="badge" data-i18n="research">Research results · provisional</span></div>
|
| 44 |
+
|
| 45 |
+
|
| 46 |
|
| 47 |
<div class="toolbar">
|
| 48 |
<div class="view-switch" role="group" aria-label="Leaderboard view">
|
| 49 |
+
<button type="button" data-view="all" aria-pressed="true" data-i18n="all">All results</button>
|
| 50 |
<button type="button" data-view="compare" aria-pressed="false" data-i18n="compare">Baseline / DeepAgents pairs</button>
|
| 51 |
<button type="button" data-view="baseline" aria-pressed="false" data-i18n="single">Single-pass Agent</button>
|
| 52 |
+
<button type="button" data-view="harness" aria-pressed="false" data-i18n="harness">All harness runs</button>
|
| 53 |
</div>
|
| 54 |
<a class="download" href="./leaderboard.csv" download data-i18n="download">Download CSV ↓</a>
|
| 55 |
</div>
|
| 56 |
<div class="filters">
|
| 57 |
+
<label class="metric-control"><span data-i18n="metric">Rank by</span><select id="metric">
|
| 58 |
<optgroup label="Local paper profile" data-group="local">
|
| 59 |
<option value="perfect" data-i18n="perfect">Full replication · local</option>
|
| 60 |
</optgroup>
|
|
|
|
| 68 |
<label id="sort-control"><span data-i18n="sort">Sort by</span><select id="sort"><option value="deepagents" data-i18n="harnessScore">DeepAgents score</option><option value="baseline" data-i18n="singleScore">Single-pass score</option><option value="gain" data-i18n="lift">Lift</option></select></label>
|
| 69 |
<label class="search-control"><span data-i18n="search">Find a model</span><input id="search" type="search" placeholder="Search models…" autocomplete="off"></label>
|
| 70 |
</div>
|
| 71 |
+
<div class="display-tools">
|
| 72 |
+
<p id="metric-description" class="metric-description"></p>
|
| 73 |
+
<div class="display-switch" role="group" aria-label="Display format">
|
| 74 |
+
<button type="button" data-display="table" aria-pressed="true" data-i18n="tableView">Table</button>
|
| 75 |
+
<button type="button" data-display="mountain" aria-pressed="false" data-i18n="mountainView">Mountain view</button>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
<p id="independent-summary" class="independent-summary" hidden><span class="independent-dot" aria-hidden="true"></span><span data-i18n="dshShort">DSH is an independent run with technical recovery, not a paired lift.</span> <a href="#protocol-details" id="show-protocol" data-i18n="detailsLink">Details ↗</a></p>
|
| 79 |
+
<figure id="ranking-mountain" class="mountain" hidden></figure>
|
| 80 |
<p id="load-state" role="status" class="load-state">Loading archived results…</p>
|
| 81 |
<div id="error" class="error" role="alert" hidden><p data-i18n="error">Results could not be loaded. Please retry or open the results archive.</p><button type="button" id="retry" data-i18n="retry">Retry</button></div>
|
| 82 |
<div id="table-wrap" class="table-wrap" tabindex="0" aria-label="Scrollable leaderboard" hidden>
|
|
|
|
| 84 |
</div>
|
| 85 |
<p id="empty" class="load-state" hidden data-i18n="empty">No matching models.</p>
|
| 86 |
<div class="table-foot"><span id="result-count" aria-live="polite"></span><span data-i18n="denominator">Every score uses all 1,000 tasks. Unknown and invalid outcomes earn no successes.</span></div>
|
| 87 |
+
<p class="publication-note" data-i18n="publicationNote">Research snapshot · local scoring; official scorer parity unverified. Run budgets differ.</p>
|
| 88 |
</section>
|
| 89 |
|
| 90 |
+
<section class="method" aria-label="Evaluation details">
|
| 91 |
+
<details id="protocol-details" class="evidence">
|
| 92 |
+
<summary data-i18n="evaluationDetails">Evaluation protocol, definitions & evidence</summary>
|
| 93 |
+
<p class="scope-note" data-i18n="scope">Locally scored, archived results. Official scorer parity is not yet verified. Different budgets and run protocols mean the lift is descriptive, not an equal-cost comparison.</p>
|
| 94 |
+
<section class="protocols" aria-label="Experimental settings">
|
| 95 |
+
<div><p class="protocol-tag baseline"><span aria-hidden="true">A</span><strong data-i18n="single">Single-pass Agent</strong></p><p data-i18n="singleDesc">One code-generation call, followed by execution. No iterative agent loop.</p></div>
|
| 96 |
+
<div><p class="protocol-tag harness"><span aria-hidden="true">B</span><strong data-i18n="deepagents">DeepAgents</strong></p><p data-i18n="harnessDesc">DeepAgents plans, uses tools and revises code. Up to 6 model calls, plus final execution.</p></div>
|
| 97 |
+
<div><p class="protocol-tag independent"><span aria-hidden="true">C</span><strong data-i18n="dsh">DeepSeek Harness · separate run</strong></p><p data-i18n="dshDesc">Official DSH SDK, requested v4-pro API alias. A fixed technical recovery selection; underlying model weights are unverified.</p></div>
|
| 98 |
+
</section>
|
| 99 |
+
<aside id="independent-notice" class="independent-notice" hidden><strong data-i18n="dshNoticeTitle">DSH is an independent result.</strong><p data-i18n="dshNotice">The request used deepseek-v4-pro; the underlying model weights were not verified. The fixed 1,000-task selection includes 87 technical replacement attempts (27 with changed input-schema prompts), with 30 task outcomes still unknown. It is excluded from the paired DeepAgents lift.</p><a id="dsh-protocol" data-i18n="dshProtocol" target="_blank" rel="noopener noreferrer">Read run protocol & evidence ↗</a></aside>
|
| 100 |
<div class="method-grid">
|
| 101 |
<article><h3 data-i18n="fixedTasks">Fixed task set</h3><p data-i18n="fixedDesc">The same Selected_1000 task IDs and dataset revision are used for every group. Each score is successes ÷ 1,000; failed or unknown tasks stay in the denominator.</p></article>
|
| 102 |
<article><h3 data-i18n="separateMetrics">Two scoring profiles</h3><p data-i18n="profilesDesc">Full replication is the local paper metric. The four HF-style metrics reproduce the published descriptions locally; they are not yet verified against the official scorer.</p></article>
|
| 103 |
<article><h3 data-i18n="budgetTitle">Read lift with the budgets</h3><p data-i18n="budgetDesc">Baseline: 1 model call. DeepAgents and DSH: up to 6 model calls and 4 trial tool calls per attempt. DSH adds 87 recovery attempts; full environment and reasoning parity is unverified. DSH has no paired lift; these are not equal-cost causal estimates.</p></article>
|
| 104 |
</div>
|
| 105 |
+
<h3 class="evidence-heading" data-i18n="evidence">Evidence status & metric definitions</h3>
|
| 106 |
<p data-i18n="unknownDesc">“Metric unknown” means the selected metric cannot be assessed, including failed execution or unavailable outputs. It is different from a task whose final status is unknown. Neither is dropped or counted as a success.</p>
|
| 107 |
<ul id="definitions"></ul>
|
| 108 |
<div class="evidence-table-wrap"><table class="evidence-table"><caption data-i18n="evidenceCaption">Status from each accepted archive</caption><thead><tr><th data-i18n="model">Model / API alias</th><th data-i18n="protocol">Protocol</th><th data-i18n="evidenceCol">Evidence</th></tr></thead><tbody id="evidence-body"></tbody></table></div>
|
| 109 |
<p data-i18n="verification">HF-style counts are checked against archived task flags. Full-replication counts for Sol and Opus come from accepted summaries; all other displayed runs, including DSH, have per-task local flags. This refresh does not rerun or rescore any experiment.</p>
|
| 110 |
<p data-i18n="archiveNote">Per-run archive files remain frozen, including their original publication metadata. This page is a new display derived from those archives. GPT-5.5 and unarchived experiments are not included in this edition.</p>
|
| 111 |
+
|
| 112 |
</details>
|
| 113 |
+
<a class="historical" href="./legacy.html" data-i18n="historical">Historical leaderboard ↗</a>
|
| 114 |
</section>
|
| 115 |
<footer><span>InferenceNet <span class="muted">/</span> <span data-i18n="footer">Econometric replication, measured.</span></span><div><a id="revision-link" href="https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/tree/main/results" target="_blank" rel="noopener noreferrer" data-i18n="source">Source snapshot ↗</a><a href="./leaderboard-data.json" data-i18n="json">Data JSON ↗</a><a href="https://easonai-5589.github.io/inferencenet-challenge/" target="_blank" rel="noopener noreferrer" data-i18n="project">Project ↗</a></div></footer>
|
| 116 |
</main>
|
leaderboard.css
CHANGED
|
@@ -1,7 +1,17 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
.
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* InferenceNet Challenge design tokens, shared with the project website. */
|
| 2 |
+
:root{color-scheme:light;--paper:#fff;--surface:#fff;--ink:#191919;--soft:#555;--muted:#666;--line:#dedede;--strong-line:#bbb;--tint:#f5f5f5;--selected:#f1f2fa;--independent:#79590d;--independent-bg:#faf6ea;--sans:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;--mono:ui-monospace,SFMono-Regular,Consolas,monospace}
|
| 3 |
+
html[data-theme=dark]{color-scheme:dark;--paper:#111;--surface:#181818;--ink:#eee;--soft:#bdbdbd;--muted:#aaa;--line:#363636;--strong-line:#666;--tint:#222;--selected:#252736;--independent:#e6c772;--independent-bg:#28251c}
|
| 4 |
+
*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:28px;-webkit-text-size-adjust:100%}body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.65;-webkit-font-smoothing:antialiased}a{color:inherit;text-decoration:none}a:hover{text-decoration:underline;text-underline-offset:4px}button,input,select{font:inherit;color:inherit}button{cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}[hidden]{display:none!important}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--ink);outline-offset:3px}h1,h2,h3,p,figure{margin:0}button{background:var(--paper);border:1px solid var(--line);border-radius:3px}button:hover{background:var(--tint)}.page{width:min(1320px,calc(100% - 80px));margin:auto}.skip{position:fixed;top:-100px;left:16px;padding:12px;background:var(--ink);color:var(--paper);z-index:10}.skip:focus{top:12px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
| 5 |
+
.topbar{display:flex;align-items:center;justify-content:space-between;gap:24px;min-height:82px;border-bottom:1px solid var(--line)}.brand{font-size:24px;font-weight:650;letter-spacing:-1px;white-space:nowrap}.brand small{font-family:var(--mono);font-size:9px;font-weight:400;letter-spacing:1px;margin-left:10px;vertical-align:middle}.nav-links{display:flex;align-items:center;gap:24px;font-size:13px;color:var(--soft)}.nav-links button{min-height:38px;padding:7px 12px;color:var(--ink)}#theme{display:flex;gap:8px;align-items:center}.theme-icon{width:14px;height:14px;border:1.5px solid currentColor;border-radius:50%;background:linear-gradient(90deg,currentColor 50%,transparent 50%)}
|
| 6 |
+
.hero{padding:37px 0 27px;border-bottom:1px solid var(--line)}.eyebrow{font-size:10px;font-weight:600;letter-spacing:.09em;color:var(--muted);text-transform:uppercase}.hero h1{font-size:clamp(28px,3.3vw,42px);font-weight:650;letter-spacing:-.035em;line-height:1.2;margin-top:13px}.intro{font-size:16px;color:var(--soft);margin-top:12px}.facts{display:flex;flex-wrap:wrap;align-items:baseline;gap:10px 25px;margin-top:22px;font-size:12px;color:var(--muted)}.facts>span{display:flex;gap:6px;align-items:baseline}.facts strong{font-weight:650;font-size:15px;color:var(--ink);font-variant-numeric:tabular-nums}.facts .update{margin-left:auto;font-size:11px}
|
| 7 |
+
#rankings{padding-top:25px}.section-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px}.badge{font-size:10px;color:var(--muted);padding:3px 7px;border:1px solid var(--line);border-radius:3px}.toolbar{display:flex;justify-content:space-between;align-items:center;gap:14px}.view-switch{display:flex;gap:20px;flex-wrap:wrap}.view-switch button{font-size:13px;padding:9px 0;border:0;border-radius:0;border-bottom:2px solid transparent;color:var(--soft);background:none}.view-switch button[aria-pressed=true]{color:var(--ink);border-bottom-color:var(--ink);font-weight:650}.download{font-size:12px;text-decoration:underline;text-underline-offset:4px;white-space:nowrap}.filters{display:flex;align-items:end;gap:16px;margin-top:19px}.filters label{display:flex;align-items:center;gap:9px;font-size:12px;color:var(--soft)}select,input{border:1px solid var(--strong-line);background:var(--paper);min-height:38px;border-radius:3px;padding:6px 10px;font-size:12px;max-width:100%}.search-control{margin-left:auto}.search-control input{width:180px}.display-tools{display:flex;gap:24px;justify-content:space-between;align-items:center;margin:14px 0 8px}.metric-description{font-size:11px;color:var(--muted);max-width:850px}.display-switch{display:flex;flex-shrink:0}.display-switch button{font-size:11px;padding:5px 11px;border-radius:0}.display-switch button:first-child{border-radius:3px 0 0 3px}.display-switch button:last-child{border-radius:0 3px 3px 0;border-left:0}.display-switch button[aria-pressed=true]{background:var(--ink);color:var(--paper);border-color:var(--ink)}.independent-summary{font-size:11px;color:var(--soft);margin:8px 0 15px;display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}.independent-summary>a{color:var(--independent);text-decoration:underline;text-underline-offset:3px}.independent-dot{width:5px;height:5px;border-radius:50%;background:var(--independent);align-self:center}.load-state,.error{padding:35px 0;color:var(--muted)}.error button{padding:6px 15px;margin-top:10px}
|
| 8 |
+
.table-wrap{overflow:auto;border-block:1px solid var(--line);overscroll-behavior-x:contain;max-width:100%;margin-top:12px}table{width:100%;border-collapse:separate;border-spacing:0;text-align:left;font-size:13px;font-variant-numeric:tabular-nums}th,td{padding:17px 12px;border-bottom:1px solid var(--line);vertical-align:middle}thead th{color:var(--soft);font-size:11px;font-weight:550;background:var(--paper);line-height:1.5}thead button{border:0;padding:0;text-align:inherit;font-size:inherit;background:transparent!important;white-space:normal}tbody tr:last-child>*{border-bottom:0}.table-wrap table{min-width:1080px}.rank{width:45px;text-align:center;color:var(--muted)}.rank-number{display:inline-flex;align-items:center;justify-content:center;width:25px;height:25px;border-radius:50%;font-size:12px}.rank-1{background:#eadcaa;color:#4d3c08;font-weight:650}.rank-2{background:#e7e8eb;color:#454850;font-weight:650}.rank-3{background:#eedecf;color:#6a4933;font-weight:650}.model-cell{font-size:13px;font-weight:600;white-space:nowrap;min-width:175px;letter-spacing:-.02em}.model-heading{min-width:175px}.numeric{text-align:right;min-width:120px}.selected-metric{background:var(--selected);font-weight:650}.metric-value{position:relative;display:inline-block;cursor:help}.numeric small{font-size:10px;margin-left:2px}.metric-tooltip{display:none;position:absolute;z-index:5;bottom:calc(100% + 8px);right:-10px;min-width:170px;max-width:220px;white-space:normal;padding:7px 10px;border:1px solid var(--strong-line);border-radius:4px;color:var(--ink);background:var(--surface);font-size:11px;font-weight:400;text-align:left;box-shadow:0 3px 10px #0001}.metric-value:hover .metric-tooltip,.metric-value:focus .metric-tooltip{display:block}.protocol-cell{min-width:120px}.protocol-label{font-size:11px;display:inline-block;border:1px solid var(--line);border-radius:3px;padding:2px 7px;white-space:nowrap;background:var(--surface)}.no-harness{color:var(--muted);border-color:transparent;background:none;padding-left:0}.independent-tag{display:block;font-size:9px;color:var(--independent);margin-top:3px;white-space:nowrap}.source-cell{font-size:10px;white-space:nowrap;color:var(--muted)}.source-cell a{display:block}.evidence-col{min-width:80px}tbody tr:hover>td:not(.selected-metric),tbody tr:hover>th{background:var(--tint)}.table-foot{display:flex;justify-content:space-between;gap:18px;font-size:11px;color:var(--muted);padding:13px 0}.table-foot #result-count{white-space:nowrap}.score-cell{text-align:right;min-width:150px}.score{font-size:17px;font-weight:600}.counts,.unknown{display:block;font-size:10px;color:var(--muted);font-weight:400}.track{display:none}.gain{font-size:17px;color:#2e6b50}.gain small{display:block;font-size:10px;color:var(--muted)}.negative{color:#9c3e35}
|
| 9 |
+
#ranking-mountain{margin-top:14px}.mountain-head{display:none!important}.mountain-caption .mountain-legend{display:none}.mountain-pill{font-size:11px!important}.mountain-pill-label{max-width:235px}.method{margin-top:30px;border-top:1px solid var(--line);padding-top:18px}.evidence summary{font-size:13px;cursor:pointer;font-weight:550;list-style-position:inside;padding:7px 0}.evidence p{font-size:12px;color:var(--soft);margin:12px 0;line-height:1.8}.scope-note{max-width:1050px}.protocols{display:grid;grid-template-columns:repeat(3,1fr);gap:28px;margin-top:22px}.protocol-tag{display:flex;gap:10px;align-items:center;color:var(--ink)!important}.protocol-tag>span{font-family:var(--mono);font-size:10px;border:1px solid var(--line);padding:0 5px}.protocol-tag strong{font-weight:600}.independent-notice{padding:15px 20px;margin:24px 0;border-left:2px solid var(--independent);background:var(--independent-bg);font-size:12px}.independent-notice p{margin:7px 0}.independent-notice a{text-decoration:underline;text-underline-offset:3px}.method-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px;margin-top:24px}.method-grid h3,.evidence-heading{font-size:13px;font-weight:600}.evidence-heading{margin-top:25px}.evidence ul{font-size:12px;color:var(--soft);padding-left:18px;line-height:1.8}.evidence li{margin:8px 0}.evidence-table-wrap{overflow:auto;margin:20px 0}.evidence-table{font-size:11px;min-width:650px}.evidence-table caption{text-align:left;color:var(--muted);padding-bottom:10px}.evidence-table td span{display:block;font-size:10px;color:var(--muted)}.evidence-table th,.evidence-table td{padding:10px}.historical{display:inline-block;font-size:11px;color:var(--muted);margin-top:14px}footer{display:flex;justify-content:space-between;gap:20px;border-top:1px solid var(--line);padding:25px 0;margin-top:30px;font-size:11px;color:var(--muted)}footer>div{display:flex;gap:20px}.muted{margin:0 5px}noscript{padding:20px}
|
| 10 |
+
@media(min-width:1800px){.page{width:min(1440px,calc(100% - 120px))}.hero{padding-top:45px}.hero h1{font-size:46px}th,td{padding-block:19px}}
|
| 11 |
+
@media(max-width:1000px){.page{width:calc(100% - 48px)}.nav-links{gap:14px}.brand small{display:none}.filters{flex-wrap:wrap}.search-control{margin-left:auto}.filters label{flex-direction:column;align-items:stretch;gap:4px}.metric-control{flex:1}.table-foot{flex-direction:column;gap:4px}.protocols,.method-grid{grid-template-columns:1fr}.protocols{gap:5px}.method-grid{gap:12px}.nav-links>a:nth-child(3){display:none}}
|
| 12 |
+
@media(max-width:620px){.page{width:calc(100% - 32px)}.topbar{flex-wrap:wrap;gap:12px;padding:18px 0;min-height:76px}.brand{font-size:21px}.nav-links{gap:8px;font-size:11px;flex-wrap:wrap;width:100%}.nav-links button{padding:5px 9px;min-height:34px}#language{margin-left:auto}.hero{padding-top:28px}.hero h1{font-size:29px}.intro{font-size:14px}.facts{gap:8px 15px;margin-top:18px}.facts .update{margin-left:0;width:100%}.toolbar{align-items:flex-start;flex-direction:column;gap:12px}.view-switch{gap:18px}.view-switch button{font-size:12px;padding:6px 0}.download{align-self:flex-end;margin-top:-3px}.filters{gap:12px}.filters label{font-size:11px}.search-control{width:100%;margin:0}.search-control input{width:100%}.display-tools{align-items:flex-start;gap:12px}.display-switch{align-self:center}.metric-description{font-size:10px}.section-head{margin-bottom:10px}.independent-summary{font-size:10px}.table-wrap table{min-width:1000px}.model-cell{min-width:160px}.numeric{min-width:108px}.table-wrap th,.table-wrap td{padding:14px 10px}footer{flex-direction:column;gap:12px}footer>div{flex-wrap:wrap;gap:14px}.mountain-pill-label{max-width:none}.mountain-caption{font-size:10px!important}}
|
| 13 |
+
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}}
|
| 14 |
+
|
| 15 |
+
.publication-note{font-size:11px;color:var(--muted);margin-top:2px}
|
| 16 |
+
|
| 17 |
+
.evidence-gap{display:block;font-size:9px;color:var(--independent);margin-top:3px}
|
leaderboard.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
'use strict';
|
| 2 |
const $ = id => document.getElementById(id);
|
| 3 |
const metricKeys = ['perfect', 'partial_replication', 'compilation_success', 'coefficient_direction', 'significance_level'];
|
|
@@ -21,9 +22,9 @@ Object.assign(en, {
|
|
| 21 |
});
|
| 22 |
const zh = {
|
| 23 |
dataset: '数据集 ↗', archive: '结果存档 ↗', edition: '研究榜单 · 2026 年 9 月',
|
| 24 |
-
title: '
|
| 25 |
-
intro: '
|
| 26 |
-
models: '种
|
| 27 |
single: '单次 Agent', harness: '全部 Harness 实验', deepagents: 'DeepAgents',
|
| 28 |
dsh: 'DeepSeek Harness · 独立实验',
|
| 29 |
dshDesc: '使用官方 DSH SDK,请求 v4-pro API 别名。按固定规则纳入技术补测,底层模型权重未经核实。',
|
|
@@ -66,13 +67,50 @@ const zh = {
|
|
| 66 |
let language = 'en';
|
| 67 |
try { language = localStorage.getItem('inferencenet-language') === 'zh' ? 'zh' : 'en'; } catch (_) {}
|
| 68 |
let data = null;
|
| 69 |
-
let view = '
|
|
|
|
|
|
|
|
|
|
| 70 |
let sortBy = 'deepagents';
|
| 71 |
const t = key => (language === 'zh' ? zh[key] : en[key]) ?? en[key] ?? key;
|
| 72 |
const esc = value => String(value).replace(/[&<>"']/g, char => ({'&':'&', '<':'<', '>':'>', '"':'"', "'":'''}[char]));
|
| 73 |
const sourceURL = (path, tree = false) => `https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/${tree ? 'tree' : 'blob'}/${data.source_revision}/${path}`;
|
| 74 |
const external = (url, label) => `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(label)} ↗</a>`;
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
function applyLanguage() {
|
| 77 |
document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
|
| 78 |
document.querySelectorAll('[data-i18n]').forEach(el => { el.innerHTML = t(el.dataset.i18n); });
|
|
@@ -83,6 +121,7 @@ function applyLanguage() {
|
|
| 83 |
document.querySelector('[data-group=local]').label = t('groupLocal');
|
| 84 |
document.querySelector('[data-group=hf]').label = t('groupHF');
|
| 85 |
$('definitions').innerHTML = metricKeys.map(key => `<li><strong>${esc(t(key))}:</strong> ${esc(t(key + 'Desc'))}</li>`).join('');
|
|
|
|
| 86 |
render();
|
| 87 |
}
|
| 88 |
|
|
@@ -113,12 +152,36 @@ function validate(candidate) {
|
|
| 113 |
// Pairing is intentionally limited to the two historical arms. Independent
|
| 114 |
// harness entries have their own rank and can never enter a paired lift.
|
| 115 |
function rankedEntries(snapshot, selectedView, key, selectedSort, query) {
|
| 116 |
-
const entries = selectedView === '
|
| 117 |
-
? snapshot.models.
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
| 120 |
const search = query.toLowerCase().trim();
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
}
|
| 123 |
|
| 124 |
function scoreCell(arm, key, style) {
|
|
@@ -135,45 +198,42 @@ function evidenceState(arm) {
|
|
| 135 |
|
| 136 |
function render() {
|
| 137 |
const key = $('metric').value;
|
| 138 |
-
$('metric-description').
|
| 139 |
if (!data) return;
|
| 140 |
-
const count = (m, arm) => m.arms[arm].metrics[key].count;
|
| 141 |
const {total, entries} = rankedEntries(data, view, key, sortBy, $('search').value);
|
| 142 |
$('published-date').textContent = t('published').replace('{date}', data.published_date);
|
| 143 |
$('sort-control').hidden = view !== 'compare';
|
| 144 |
-
$('table-wrap').hidden = entries.length === 0;
|
| 145 |
$('empty').hidden = entries.length !== 0;
|
| 146 |
$('result-count').textContent = t(view === 'compare' ? 'matched' : 'matchedRuns').replace('{n}', entries.length).replace('{total}', total);
|
| 147 |
-
$('caption').textContent = `${t(view === 'compare' ? 'compare' : view === 'baseline' ? 'single' : 'harness')} — ${t(key)}`;
|
| 148 |
const th = (text, cls = '') => `<th scope="col" class="${cls}">${esc(text)}</th>`;
|
| 149 |
-
$('table-head').innerHTML = '<tr>' + th(t('rank'), 'rank') + th(t('model')
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
lastValue = currentValue;
|
| 155 |
const arms = model.arms;
|
| 156 |
-
const
|
| 157 |
-
const badge = gap ? `<span class="tag">${esc(t('review'))}</span>` : '';
|
| 158 |
-
const independent = armId === 'dsh';
|
| 159 |
-
const protocol = view === 'harness' ? `<span class="run-protocol ${independent ? 'independent' : 'harness'}">${esc(independent ? t('independentRun') : 'DeepAgents')}</span>` : '';
|
| 160 |
-
const identity = `<td class="rank">${String(rank).padStart(2, '0')}</td><th scope="row" class="model-cell"><div class="model-name">${esc(name)}</div>${protocol}<div class="model-meta">1,000 ${language === 'zh' ? '题 / 组' : 'tasks / group'} ${badge}</div></th>`;
|
| 161 |
let scores = '', sources = '';
|
| 162 |
if (view === 'compare') {
|
| 163 |
-
const gain = (count
|
| 164 |
-
scores = scoreCell(arms.baseline, key, 'baseline') + scoreCell(arms.deepagents, key, 'harness') + `<td class="gain ${gain < 0 ? 'negative' : ''}">${gain > 0 ? '+' : ''}${gain.toFixed(1)}<small>${esc(t('pp'))}</small></td>`;
|
| 165 |
sources = external(sourceURL(arms.baseline.path, true), 'A') + external(sourceURL(arms.deepagents.path, true), 'B');
|
| 166 |
} else {
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
}
|
| 171 |
return `<tr data-model="${esc(model.id)}" data-arm="${esc(armId || 'paired')}">${identity}${scores}<td class="source-cell">${sources}</td></tr>`;
|
| 172 |
}).join('');
|
| 173 |
$('evidence-body').innerHTML = data.models.flatMap(m => Object.entries(m.arms).map(([id, arm]) => `<tr><th scope="row">${esc(arm.display_name || m.name)}</th><td>${esc(id === 'baseline' ? t('single') : id === 'dsh' ? t('dsh') : 'DeepAgents')}</td>${evidenceState(arm)}</tr>`)).join('');
|
| 174 |
const dsh = data.models.find(m => m.id === 'deepseek-v4-pro')?.arms.dsh;
|
| 175 |
$('independent-notice').hidden = !dsh;
|
|
|
|
| 176 |
if (dsh) $('dsh-protocol').href = sourceURL(dsh.protocol_url);
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
|
| 179 |
async function loadData() {
|
|
@@ -182,6 +242,8 @@ async function loadData() {
|
|
| 182 |
$('error').hidden = true;
|
| 183 |
$('load-state').hidden = false;
|
| 184 |
$('table-wrap').hidden = true;
|
|
|
|
|
|
|
| 185 |
$('empty').hidden = true;
|
| 186 |
$('result-count').textContent = '';
|
| 187 |
$('evidence-body').replaceChildren();
|
|
@@ -208,6 +270,7 @@ async function loadData() {
|
|
| 208 |
|
| 209 |
document.querySelectorAll('[data-view]').forEach(button => button.addEventListener('click', () => {
|
| 210 |
view = button.dataset.view;
|
|
|
|
| 211 |
document.querySelectorAll('[data-view]').forEach(el => el.setAttribute('aria-pressed', String(el === button)));
|
| 212 |
render();
|
| 213 |
}));
|
|
@@ -220,5 +283,23 @@ $('language').addEventListener('click', () => {
|
|
| 220 |
try { localStorage.setItem('inferencenet-language', language); } catch (_) {}
|
| 221 |
applyLanguage();
|
| 222 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
applyLanguage();
|
| 224 |
loadData();
|
|
|
|
| 1 |
+
import { mountain } from './mountain.js';
|
| 2 |
'use strict';
|
| 3 |
const $ = id => document.getElementById(id);
|
| 4 |
const metricKeys = ['perfect', 'partial_replication', 'compilation_success', 'coefficient_direction', 'significance_level'];
|
|
|
|
| 22 |
});
|
| 23 |
const zh = {
|
| 24 |
dataset: '数据集 ↗', archive: '结果存档 ↗', edition: '研究榜单 · 2026 年 9 月',
|
| 25 |
+
title: 'InferenceNet 挑战赛排行榜',
|
| 26 |
+
intro: '模型与 Harness 在 1,000 道计量经济学复现任务上的表现。',
|
| 27 |
+
models: '种模型别名', groups: '组实验', tasks: '题 / 组', records: '条任务记录', published: '发布于 {date}',
|
| 28 |
single: '单次 Agent', harness: '全部 Harness 实验', deepagents: 'DeepAgents',
|
| 29 |
dsh: 'DeepSeek Harness · 独立实验',
|
| 30 |
dshDesc: '使用官方 DSH SDK,请求 v4-pro API 别名。按固定规则纳入技术补测,底层模型权重未经核实。',
|
|
|
|
| 67 |
let language = 'en';
|
| 68 |
try { language = localStorage.getItem('inferencenet-language') === 'zh' ? 'zh' : 'en'; } catch (_) {}
|
| 69 |
let data = null;
|
| 70 |
+
let view = 'all';
|
| 71 |
+
let display = 'table';
|
| 72 |
+
let theme = 'light';
|
| 73 |
+
try { theme = localStorage.getItem('inferencenet-theme') === 'dark' ? 'dark' : 'light'; } catch (_) {}
|
| 74 |
let sortBy = 'deepagents';
|
| 75 |
const t = key => (language === 'zh' ? zh[key] : en[key]) ?? en[key] ?? key;
|
| 76 |
const esc = value => String(value).replace(/[&<>"']/g, char => ({'&':'&', '<':'<', '>':'>', '"':'"', "'":'''}[char]));
|
| 77 |
const sourceURL = (path, tree = false) => `https://huggingface.co/spaces/CamoAiLab/InferenceNet-Leaderboard/${tree ? 'tree' : 'blob'}/${data.source_revision}/${path}`;
|
| 78 |
const external = (url, label) => `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(label)} ↗</a>`;
|
| 79 |
|
| 80 |
+
|
| 81 |
+
Object.assign(en, {
|
| 82 |
+
publicationNote: 'Research snapshot · local scoring; official scorer parity unverified. Run budgets differ.',
|
| 83 |
+
all: 'All results', website: 'Project website ↗', tableView: 'Table', mountainView: 'Mountain view',
|
| 84 |
+
evaluationDetails: 'Evaluation protocol, definitions & evidence', detailsLink: 'Details ↗',
|
| 85 |
+
dshShort: 'DSH: model weights unverified; 87 technical replacements, 30 unknown task outcomes. Excluded from paired lift.',
|
| 86 |
+
harnessColumn: 'Harness', noHarness: 'None', independentLabel: 'Independent',
|
| 87 |
+
light: 'Light', dark: 'Dark', fullShort: 'Full replication', partialShort: 'Partial replication',
|
| 88 |
+
executionShort: 'Execution success', directionShort: 'Coefficient direction', significanceShort: 'Significance level',
|
| 89 |
+
mountainTitle: 'The climb to full replication', summit: '100% · every task succeeds',
|
| 90 |
+
mountainNote: 'Height represents the selected score. Run protocols and budgets differ.',
|
| 91 |
+
scoreDetails: 'Successes / tasks; metric unknown', comparisonNote: 'Paired view includes baseline and DeepAgents only. DSH remains a separate result.',
|
| 92 |
+
});
|
| 93 |
+
Object.assign(zh, {
|
| 94 |
+
publicationNote: '研究快照 · 本地评分,尚未验证与官方评分器一致;各组运行预算存在差异。',
|
| 95 |
+
all: '全部结果', website: '项目官网 ↗', tableView: '成绩表', mountainView: '登山图',
|
| 96 |
+
evaluationDetails: '评测协议、指标定义与证据', detailsLink: '查看详情 ↗',
|
| 97 |
+
dshShort: 'DSH:模型权重未核实,含 87 次技术补测替换、30 题状态未知;不纳入配对提升。',
|
| 98 |
+
harnessColumn: 'Harness', noHarness: '无', independentLabel: '独立实验',
|
| 99 |
+
light: '浅色', dark: '深色', fullShort: '完整复现率', partialShort: '部分复现率',
|
| 100 |
+
executionShort: '执行成功率', directionShort: '系数方向', significanceShort: '显著性水平',
|
| 101 |
+
mountainTitle: '向完整复现迈进', summit: '100% · 每道任务均成功',
|
| 102 |
+
mountainNote: '高度表示所选指标得分。各组运行协议与预算存在差异。',
|
| 103 |
+
scoreDetails: '成功数 / 题数;指标未知', comparisonNote: '配对视图仅比较单次组与 DeepAgents;DSH 单独展示。',
|
| 104 |
+
metric: '排名指标', single: '单次生成', harness: '模型 + Harness', compare: '配对提升',
|
| 105 |
+
});
|
| 106 |
+
const shortKeys = ['fullShort', 'partialShort', 'executionShort', 'directionShort', 'significanceShort'];
|
| 107 |
+
function applyTheme() {
|
| 108 |
+
document.documentElement.dataset.theme = theme;
|
| 109 |
+
$('theme').setAttribute('aria-pressed', String(theme === 'dark'));
|
| 110 |
+
$('theme-label').textContent = t(theme);
|
| 111 |
+
$('theme').setAttribute('aria-label', language === 'zh' ? `切换到${theme === 'light' ? '深色' : '浅色'}主题` : `Switch to ${theme === 'light' ? 'dark' : 'light'} theme`);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
function applyLanguage() {
|
| 115 |
document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
|
| 116 |
document.querySelectorAll('[data-i18n]').forEach(el => { el.innerHTML = t(el.dataset.i18n); });
|
|
|
|
| 121 |
document.querySelector('[data-group=local]').label = t('groupLocal');
|
| 122 |
document.querySelector('[data-group=hf]').label = t('groupHF');
|
| 123 |
$('definitions').innerHTML = metricKeys.map(key => `<li><strong>${esc(t(key))}:</strong> ${esc(t(key + 'Desc'))}</li>`).join('');
|
| 124 |
+
applyTheme();
|
| 125 |
render();
|
| 126 |
}
|
| 127 |
|
|
|
|
| 152 |
// Pairing is intentionally limited to the two historical arms. Independent
|
| 153 |
// harness entries have their own rank and can never enter a paired lift.
|
| 154 |
function rankedEntries(snapshot, selectedView, key, selectedSort, query) {
|
| 155 |
+
const entries = selectedView === 'compare'
|
| 156 |
+
? snapshot.models.map(model => ({model, armId: null, arm: model.arms.baseline, name: model.name,
|
| 157 |
+
value: selectedSort === 'gain' ? model.arms.deepagents.metrics[key].count - model.arms.baseline.metrics[key].count : model.arms[selectedSort].metrics[key].count}))
|
| 158 |
+
: snapshot.models.flatMap(model => Object.entries(model.arms)
|
| 159 |
+
.filter(([id]) => selectedView === 'all' || (selectedView === 'baseline' ? id === 'baseline' : id !== 'baseline'))
|
| 160 |
+
.map(([armId, arm]) => ({model, armId, arm, name: armId === 'dsh' ? 'DeepSeek API · v4-pro alias' : model.name, value: arm.metrics[key].count})));
|
| 161 |
const search = query.toLowerCase().trim();
|
| 162 |
+
const filtered = entries.filter(entry => `${entry.name} ${entry.model.id} ${entry.armId || ''} ${entry.arm.display_name || ''}`.toLowerCase().includes(search))
|
| 163 |
+
.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name) || String(a.armId).localeCompare(String(b.armId)));
|
| 164 |
+
let last = null, rank = 0;
|
| 165 |
+
filtered.forEach((entry, i) => { if (entry.value !== last) rank = i + 1; last = entry.value; entry.rank = rank; });
|
| 166 |
+
return {total: entries.length, entries: filtered};
|
| 167 |
+
}
|
| 168 |
+
function protocolName(armId) { return armId === 'baseline' ? t('noHarness') : armId === 'dsh' ? 'DSH' : 'DeepAgents'; }
|
| 169 |
+
function compactScore(arm, key, active) {
|
| 170 |
+
const m = arm.metrics[key];
|
| 171 |
+
const description = `${m.count} / 1,000 ${t('passed')}; ${t('metricUnknown')}: ${m.unknown}`;
|
| 172 |
+
return `<td class="numeric ${active ? 'selected-metric' : ''}"><span tabindex="0" class="metric-value" aria-label="${esc(m.score.toFixed(1) + '%; ' + description)}">${m.score.toFixed(1)}<small>%</small><span class="metric-tooltip" role="tooltip">${esc(description)}</span></span></td>`;
|
| 173 |
+
}
|
| 174 |
+
function renderMountain(entries, key) {
|
| 175 |
+
const el = $('ranking-mountain');
|
| 176 |
+
el.hidden = display !== 'mountain' || entries.length === 0 || view === 'compare';
|
| 177 |
+
if (el.hidden) return;
|
| 178 |
+
// Mountain markers use unique ordinal IDs, with tied scores at the same altitude.
|
| 179 |
+
mountain(el, {
|
| 180 |
+
title: t('mountainTitle'), metric: t(shortKeys[metricKeys.indexOf(key)]), unit: '%', max: 100,
|
| 181 |
+
peakLabel: t('summit'), caption: t('mountainNote'),
|
| 182 |
+
items: entries.map((entry, i) => ({rank: i + 1, label: `${entry.name} · ${protocolName(entry.armId)}${entry.armId === 'dsh' ? ' *' : ''}`, value: entry.arm.metrics[key].score, kind: 'internal'})),
|
| 183 |
+
locale: language,
|
| 184 |
+
}, {table: false, aspect: 0.44, compactAspect: 0.75});
|
| 185 |
}
|
| 186 |
|
| 187 |
function scoreCell(arm, key, style) {
|
|
|
|
| 198 |
|
| 199 |
function render() {
|
| 200 |
const key = $('metric').value;
|
| 201 |
+
$('metric-description').textContent = t(key + 'Desc');
|
| 202 |
if (!data) return;
|
|
|
|
| 203 |
const {total, entries} = rankedEntries(data, view, key, sortBy, $('search').value);
|
| 204 |
$('published-date').textContent = t('published').replace('{date}', data.published_date);
|
| 205 |
$('sort-control').hidden = view !== 'compare';
|
| 206 |
+
$('table-wrap').hidden = entries.length === 0 || (display === 'mountain' && view !== 'compare');
|
| 207 |
$('empty').hidden = entries.length !== 0;
|
| 208 |
$('result-count').textContent = t(view === 'compare' ? 'matched' : 'matchedRuns').replace('{n}', entries.length).replace('{total}', total);
|
| 209 |
+
$('caption').textContent = `${t(view === 'compare' ? 'compare' : view === 'all' ? 'all' : view === 'baseline' ? 'single' : 'harness')} — ${t(key)}`;
|
| 210 |
const th = (text, cls = '') => `<th scope="col" class="${cls}">${esc(text)}</th>`;
|
| 211 |
+
$('table-head').innerHTML = '<tr>' + th(t('rank'), 'rank') + th(t('model'), 'model-heading') + (view === 'compare'
|
| 212 |
+
? th(t('single'), 'numeric') + th('DeepAgents', 'numeric') + th(t('lift'), 'numeric')
|
| 213 |
+
: th(t('harnessColumn')) + metricKeys.map((metric, i) => `<th scope="col" class="numeric ${metric === key ? 'selected-metric' : ''}" aria-sort="${metric === key ? 'descending' : 'none'}"><button type="button" data-metric="${metric}" title="${esc(t(metric + 'Desc'))}">${esc(t(shortKeys[i]))}${metric === key ? ' ↓' : ''}</button></th>`).join('')) + th(t('evidenceCol'), 'evidence-col') + '</tr>';
|
| 214 |
+
$('table-body').innerHTML = entries.map(entry => {
|
| 215 |
+
const {model, arm, armId, name, rank} = entry;
|
|
|
|
| 216 |
const arms = model.arms;
|
| 217 |
+
const identity = `<td class="rank"><span class="rank-number rank-${rank}">${rank}</span></td><th scope="row" class="model-cell">${esc(name)}</th>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
let scores = '', sources = '';
|
| 219 |
if (view === 'compare') {
|
| 220 |
+
const gain = (arms.deepagents.metrics[key].count - arms.baseline.metrics[key].count) / 10;
|
| 221 |
+
scores = scoreCell(arms.baseline, key, 'baseline') + scoreCell(arms.deepagents, key, 'harness') + `<td class="gain numeric ${gain < 0 ? 'negative' : ''}">${gain > 0 ? '+' : ''}${gain.toFixed(1)}<small>${esc(t('pp'))}</small></td>`;
|
| 222 |
sources = external(sourceURL(arms.baseline.path, true), 'A') + external(sourceURL(arms.deepagents.path, true), 'B');
|
| 223 |
} else {
|
| 224 |
+
const badge = armId === 'dsh' ? `<a class="independent-tag" href="#protocol-details">${esc(t('independentLabel'))} ↗</a>` : '';
|
| 225 |
+
scores = `<td class="protocol-cell"><span class="protocol-label ${armId === 'baseline' ? 'no-harness' : ''}">${esc(protocolName(armId))}</span>${badge}</td>` + metricKeys.map(metric => compactScore(arm, metric, metric === key)).join('');
|
| 226 |
+
sources = external(sourceURL(arm.path + '/results.jsonl'), t('recordsLink')) + (!arm.complete ? `<span class="evidence-gap">${esc(t('review'))}</span>` : '');
|
| 227 |
}
|
| 228 |
return `<tr data-model="${esc(model.id)}" data-arm="${esc(armId || 'paired')}">${identity}${scores}<td class="source-cell">${sources}</td></tr>`;
|
| 229 |
}).join('');
|
| 230 |
$('evidence-body').innerHTML = data.models.flatMap(m => Object.entries(m.arms).map(([id, arm]) => `<tr><th scope="row">${esc(arm.display_name || m.name)}</th><td>${esc(id === 'baseline' ? t('single') : id === 'dsh' ? t('dsh') : 'DeepAgents')}</td>${evidenceState(arm)}</tr>`)).join('');
|
| 231 |
const dsh = data.models.find(m => m.id === 'deepseek-v4-pro')?.arms.dsh;
|
| 232 |
$('independent-notice').hidden = !dsh;
|
| 233 |
+
$('independent-summary').hidden = !dsh || view === 'baseline' || view === 'compare';
|
| 234 |
if (dsh) $('dsh-protocol').href = sourceURL(dsh.protocol_url);
|
| 235 |
+
document.querySelector('[data-display="mountain"]').disabled = view === 'compare';
|
| 236 |
+
renderMountain(entries, key);
|
| 237 |
}
|
| 238 |
|
| 239 |
async function loadData() {
|
|
|
|
| 242 |
$('error').hidden = true;
|
| 243 |
$('load-state').hidden = false;
|
| 244 |
$('table-wrap').hidden = true;
|
| 245 |
+
$('ranking-mountain').hidden = true;
|
| 246 |
+
$('independent-summary').hidden = true;
|
| 247 |
$('empty').hidden = true;
|
| 248 |
$('result-count').textContent = '';
|
| 249 |
$('evidence-body').replaceChildren();
|
|
|
|
| 270 |
|
| 271 |
document.querySelectorAll('[data-view]').forEach(button => button.addEventListener('click', () => {
|
| 272 |
view = button.dataset.view;
|
| 273 |
+
if (view === 'compare') setDisplay('table');
|
| 274 |
document.querySelectorAll('[data-view]').forEach(el => el.setAttribute('aria-pressed', String(el === button)));
|
| 275 |
render();
|
| 276 |
}));
|
|
|
|
| 283 |
try { localStorage.setItem('inferencenet-language', language); } catch (_) {}
|
| 284 |
applyLanguage();
|
| 285 |
});
|
| 286 |
+
function setDisplay(next) {
|
| 287 |
+
display = next;
|
| 288 |
+
document.querySelectorAll('[data-display]').forEach(el => el.setAttribute('aria-pressed', String(el.dataset.display === next)));
|
| 289 |
+
render();
|
| 290 |
+
}
|
| 291 |
+
document.querySelectorAll('[data-display]').forEach(button => button.addEventListener('click', () => setDisplay(button.dataset.display)));
|
| 292 |
+
$('table-head').addEventListener('click', event => {
|
| 293 |
+
const button = event.target.closest('[data-metric]');
|
| 294 |
+
if (button) { $('metric').value = button.dataset.metric; render(); }
|
| 295 |
+
});
|
| 296 |
+
document.addEventListener('click', event => {
|
| 297 |
+
if (event.target.closest('a[href="#protocol-details"]')) $('protocol-details').open = true;
|
| 298 |
+
});
|
| 299 |
+
$('theme').addEventListener('click', () => {
|
| 300 |
+
theme = theme === 'light' ? 'dark' : 'light';
|
| 301 |
+
try { localStorage.setItem('inferencenet-theme', theme); } catch (_) {}
|
| 302 |
+
applyTheme();
|
| 303 |
+
});
|
| 304 |
applyLanguage();
|
| 305 |
loadData();
|
mountain.css
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Mountain-climb leaderboard. Companion to mountain.js. Uses the site tokens from
|
| 2 |
+
* styles.css (--paper, --surface, --ink, --soft, --muted, --line, --strong-line, --tint, --sans, --mono)
|
| 3 |
+
* and defines every colour the drawing needs on the component itself, for light and dark. */
|
| 4 |
+
.mountain{
|
| 5 |
+
--mtn-sky:#e9eef5;--mtn-sky-low:#ffffff;
|
| 6 |
+
--mtn-far:#e3e8ef;--mtn-mid:#d2dae4;--mtn-near:#b9c4d1;--mtn-shade:#6f7f93;--mtn-foot:#a2aebd;
|
| 7 |
+
--mtn-snow:#ffffff;--mtn-cap:#ffffff;--mtn-outline:#4b5665;--mtn-haze:#ffffff;
|
| 8 |
+
--mtn-line:#7d8794;--mtn-route:#2f3742;--mtn-flag:#d6382a;--mtn-pole:#262a30;
|
| 9 |
+
--mtn-gold:#e8b421;--mtn-silver:#c9d0da;--mtn-bronze:#c98a52;--mtn-on-gold:#1a1a1a;
|
| 10 |
+
--mtn-marker:#28313c;--mtn-marker-text:#ffffff;--mtn-marker-ring:#ffffff;--mtn-focus:#191919;
|
| 11 |
+
--mtn-pill:#20272f;--mtn-pill-text:#ffffff;--mtn-pill-edge:rgba(255,255,255,.55);--mtn-badge:rgba(255,255,255,.16);
|
| 12 |
+
--mtn-text:#191919;--mtn-muted:#5d6672;--mtn-halo:rgba(255,255,255,.9);--mtn-trail:#1f262e;
|
| 13 |
+
display:block;margin:0;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--ink);overflow:hidden;font-family:var(--sans,Inter,Arial,sans-serif)}
|
| 14 |
+
html[data-theme="dark"] .mountain{
|
| 15 |
+
--mtn-sky:#0d121a;--mtn-sky-low:#171b22;
|
| 16 |
+
--mtn-far:#1e2632;--mtn-mid:#293340;--mtn-near:#38434f;--mtn-shade:#141a22;--mtn-foot:#28313c;
|
| 17 |
+
--mtn-snow:#d5dde7;--mtn-cap:#c9d2dd;--mtn-outline:#8892a0;--mtn-haze:#171b22;
|
| 18 |
+
--mtn-line:#6c7684;--mtn-route:#dfe5ec;--mtn-flag:#e4493a;--mtn-pole:#e8e8e8;
|
| 19 |
+
--mtn-gold:#f2c13d;--mtn-silver:#aeb7c3;--mtn-bronze:#cf925c;--mtn-on-gold:#1a1a1a;
|
| 20 |
+
--mtn-marker:#e9edf2;--mtn-marker-text:#151515;--mtn-marker-ring:#111111;--mtn-focus:#eeeeee;
|
| 21 |
+
--mtn-pill:#e9ebee;--mtn-pill-text:#151515;--mtn-pill-edge:rgba(0,0,0,.55);--mtn-badge:rgba(0,0,0,.12);
|
| 22 |
+
--mtn-text:#eeeeee;--mtn-muted:#a5adb8;--mtn-halo:rgba(17,17,17,.9);--mtn-trail:#e6eaef}
|
| 23 |
+
|
| 24 |
+
.mountain>script{display:none}
|
| 25 |
+
.mountain-canvas{display:block}
|
| 26 |
+
|
| 27 |
+
/* Header and caption. */
|
| 28 |
+
.mountain-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;padding:18px 22px 14px;border-bottom:1px solid var(--line)}
|
| 29 |
+
.mountain-title{margin:0;font-size:17px;font-weight:600;letter-spacing:-.015em;line-height:1.35;color:var(--ink)}
|
| 30 |
+
.mountain-subtitle{margin:4px 0 0;font-size:13px;line-height:1.6;color:var(--muted)}
|
| 31 |
+
.mountain-stamp{margin:0;font-family:var(--mono,ui-monospace,monospace);font-size:12px;line-height:1.6;color:var(--muted);white-space:nowrap}
|
| 32 |
+
.mountain-caption{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px 20px;padding:12px 22px 14px;border-top:1px solid var(--line);font-size:12px;line-height:1.6;color:var(--muted)}
|
| 33 |
+
.mountain-note{margin:0}
|
| 34 |
+
.mountain-legend{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px 16px}
|
| 35 |
+
.mountain-legend li{display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
|
| 36 |
+
.mtn-key{display:inline-block;width:11px;height:11px;border-radius:50%;background:var(--mtn-marker);border:2px solid var(--mtn-marker-ring);box-sizing:content-box}
|
| 37 |
+
.mtn-key--gold{background:var(--mtn-gold)}
|
| 38 |
+
.mtn-key--internal{border:1.5px dashed var(--mtn-marker);background:transparent;width:10px;height:10px}
|
| 39 |
+
.mtn-key--trail{width:20px;height:0;border:0;border-top:2px dashed var(--mtn-trail);border-radius:0;background:transparent}
|
| 40 |
+
|
| 41 |
+
/* Stage: the drawing plus the pill overlay. */
|
| 42 |
+
.mountain-stage{position:relative;display:block;width:100%;line-height:0}
|
| 43 |
+
.mountain-svg{display:block;width:100%;height:auto;font-family:inherit;font-variant-numeric:tabular-nums}
|
| 44 |
+
.mountain-svg text{font-family:inherit;line-height:1}
|
| 45 |
+
|
| 46 |
+
/* Terrain. */
|
| 47 |
+
.mtn-sky-top{stop-color:var(--mtn-sky)}
|
| 48 |
+
.mtn-sky-low{stop-color:var(--mtn-sky-low)}
|
| 49 |
+
.mtn-haze-top{stop-color:var(--mtn-haze);stop-opacity:0}
|
| 50 |
+
.mtn-haze-low{stop-color:var(--mtn-haze);stop-opacity:.55}
|
| 51 |
+
.mtn-far{fill:var(--mtn-far)}
|
| 52 |
+
.mtn-mid{fill:var(--mtn-mid)}
|
| 53 |
+
.mtn-cap{fill:var(--mtn-cap);opacity:.85}
|
| 54 |
+
.mtn-near{fill:var(--mtn-near)}
|
| 55 |
+
.mtn-snow{fill:var(--mtn-snow)}
|
| 56 |
+
.mtn-shade{fill:var(--mtn-shade);opacity:.28}
|
| 57 |
+
.mtn-stria{fill:none;stroke:var(--mtn-outline);stroke-width:1;opacity:.14;stroke-linejoin:round}
|
| 58 |
+
.mtn-foot{fill:var(--mtn-foot)}
|
| 59 |
+
.mtn-outline{fill:none;stroke:var(--mtn-outline);stroke-width:1;stroke-linejoin:round;opacity:.42}
|
| 60 |
+
.mtn-outline--foot{opacity:.22}
|
| 61 |
+
.mtn-contour{stroke:var(--mtn-line);stroke-width:1;stroke-dasharray:2 7;stroke-linecap:round;opacity:.38}
|
| 62 |
+
.mtn-contour-label{fill:var(--mtn-muted);font-size:12px;paint-order:stroke;stroke:var(--mtn-halo);stroke-width:3px;stroke-linejoin:round}
|
| 63 |
+
.mtn-base{stroke:var(--mtn-line);stroke-width:1}
|
| 64 |
+
.mtn-route{stroke:var(--mtn-route);stroke-width:2.2;stroke-dasharray:0 7;stroke-linecap:round;opacity:.72}
|
| 65 |
+
.mtn-pole{stroke:var(--mtn-pole);stroke-width:2;stroke-linecap:round}
|
| 66 |
+
.mtn-flag{fill:var(--mtn-flag)}
|
| 67 |
+
.mtn-peak-label{fill:var(--mtn-text);font-size:12px;font-weight:500;paint-order:stroke;stroke:var(--mtn-halo);stroke-width:3px;stroke-linejoin:round}
|
| 68 |
+
.mtn-stamp{fill:var(--mtn-muted);font-family:var(--mono,ui-monospace,monospace);font-size:12px}
|
| 69 |
+
|
| 70 |
+
/* Connectors, trails. */
|
| 71 |
+
.mtn-connector{stroke:var(--mtn-line);stroke-width:1;opacity:.5;transition:opacity .15s,stroke-width .15s}
|
| 72 |
+
.mtn-connector.is-active{stroke:var(--mtn-focus);stroke-width:1.5;opacity:1}
|
| 73 |
+
.mtn-trail-path{stroke:var(--mtn-trail);stroke-width:1.75;stroke-dasharray:6 5;stroke-linecap:round}
|
| 74 |
+
.mtn-trail-head{fill:var(--mtn-trail)}
|
| 75 |
+
.mtn-trail-label{fill:var(--mtn-text);font-size:12px;font-weight:600;paint-order:stroke;stroke:var(--mtn-halo);stroke-width:4px;stroke-linejoin:round}
|
| 76 |
+
|
| 77 |
+
/* Markers. */
|
| 78 |
+
.mtn-marker{cursor:default}
|
| 79 |
+
.mtn-marker-dot{fill:var(--mtn-marker);stroke:var(--mtn-marker-ring);stroke-width:2;transition:stroke-width .15s}
|
| 80 |
+
.mtn-marker--leader .mtn-marker-dot{fill:var(--mtn-gold);stroke-width:2.5}
|
| 81 |
+
.mtn-marker--silver .mtn-marker-dot{stroke:var(--mtn-silver);stroke-width:3.5}
|
| 82 |
+
.mtn-marker--bronze .mtn-marker-dot{stroke:var(--mtn-bronze);stroke-width:3.5}
|
| 83 |
+
.mtn-marker--waypoint .mtn-marker-dot{fill:var(--mtn-sky-low);stroke:var(--mtn-marker);stroke-width:1.5}
|
| 84 |
+
.mtn-marker-num{fill:var(--mtn-marker-text);font-size:12px;font-weight:700;pointer-events:none}
|
| 85 |
+
.mtn-marker--leader .mtn-marker-num{fill:var(--mtn-on-gold);font-size:14px}
|
| 86 |
+
.mtn-marker-halo{fill:none;stroke:var(--mtn-focus);stroke-width:2;opacity:0;transition:opacity .15s}
|
| 87 |
+
.mtn-marker--leader .mtn-marker-halo{stroke:var(--mtn-gold);stroke-width:1.5;opacity:.55}
|
| 88 |
+
.mtn-marker.is-active .mtn-marker-halo{stroke:var(--mtn-focus);stroke-width:2;opacity:1}
|
| 89 |
+
.mtn-marker-ring{fill:none;stroke:var(--mtn-marker);stroke-width:1.25;stroke-dasharray:3 3}
|
| 90 |
+
.mtn-marker-val{fill:var(--mtn-text);font-size:12px;font-weight:600;paint-order:stroke;stroke:var(--mtn-halo);stroke-width:4px;stroke-linejoin:round;opacity:0;transition:opacity .15s;pointer-events:none}
|
| 91 |
+
.mtn-marker.is-active .mtn-marker-val{opacity:1}
|
| 92 |
+
|
| 93 |
+
/* Pills: absolute overlay in the wide layout, ranks stacked left (even) and right (odd). */
|
| 94 |
+
.mountain-pills{list-style:none;margin:0;padding:0;position:absolute;inset:0;pointer-events:none;line-height:1}
|
| 95 |
+
.mountain-pill{position:absolute;display:inline-flex;align-items:center;gap:8px;height:28px;padding:0 11px 0 4px;border-radius:14px;background:var(--mtn-pill);color:var(--mtn-pill-text);border:1px solid var(--mtn-pill-edge);font-size:13px;font-weight:600;white-space:nowrap;pointer-events:auto;cursor:default;box-sizing:border-box;transition:transform .15s,border-color .15s}
|
| 96 |
+
.mountain-pill--right{flex-direction:row-reverse;padding:0 4px 0 11px}
|
| 97 |
+
.mountain-pill:hover,.mountain-pill.is-active{border-color:var(--mtn-focus);transform:translateY(-1px)}
|
| 98 |
+
.mountain-pill:focus{outline:none}
|
| 99 |
+
.mountain-pill:focus-visible{outline:2px solid var(--mtn-focus);outline-offset:3px}
|
| 100 |
+
.mountain-pill--internal .mountain-badge{box-shadow:inset 0 0 0 1.5px var(--mtn-pill-edge)}
|
| 101 |
+
.mountain-badge{display:inline-grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--mtn-badge);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;flex:none}
|
| 102 |
+
.mountain-badge--gold{background:var(--mtn-gold);color:var(--mtn-on-gold)}
|
| 103 |
+
.mountain-badge--silver{background:var(--mtn-silver);color:var(--mtn-on-gold)}
|
| 104 |
+
.mountain-badge--bronze{background:var(--mtn-bronze);color:var(--mtn-on-gold)}
|
| 105 |
+
.mountain-pill-value{display:none}
|
| 106 |
+
/* Narrow desktop (700–1000px container): smaller pills leave the drawing more width. */
|
| 107 |
+
.mountain--narrow .mountain-pill{font-size:12px;padding:0 9px 0 4px}
|
| 108 |
+
.mountain--narrow .mountain-pill--right{padding:0 4px 0 9px}
|
| 109 |
+
.mountain-vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}
|
| 110 |
+
|
| 111 |
+
/* Compact layout (container narrower than the breakpoint): 4:3 drawing, pills listed below. */
|
| 112 |
+
.mountain--compact .mountain-head{flex-direction:column;gap:2px;padding:16px 16px 12px}
|
| 113 |
+
.mountain--compact .mountain-stamp{white-space:normal;margin-top:6px}
|
| 114 |
+
.mountain--compact .mountain-caption{padding:12px 16px}
|
| 115 |
+
.mountain--compact .mountain-pills{position:static;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;padding:14px 16px 4px;pointer-events:auto;border-top:1px solid var(--line)}
|
| 116 |
+
.mountain--compact .mountain-pill{position:static;display:flex;flex-direction:row;height:auto;min-height:32px;padding:5px 10px 5px 5px;border-radius:16px;white-space:normal;font-size:12px;line-height:1.3;gap:7px}
|
| 117 |
+
.mountain--compact .mountain-pill-label{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
|
| 118 |
+
.mountain--compact .mountain-pill-value{display:inline;font-variant-numeric:tabular-nums;font-weight:500;opacity:.8;flex:none}
|
| 119 |
+
.mountain--compact .mountain-pill:hover,.mountain--compact .mountain-pill.is-active{transform:none}
|
| 120 |
+
@media(max-width:420px){.mountain--compact .mountain-pills{grid-template-columns:1fr}} /* one column on phones: labels stay on one or two lines */
|
| 121 |
+
|
| 122 |
+
/* Ranked table (no-JS fallback and screen readers). An authored table with more than
|
| 123 |
+
* three columns goes inside <div class="mountain-table-wrap"> so it scrolls sideways on phones. */
|
| 124 |
+
.mountain-table-wrap{max-width:100%;overflow-x:auto;overscroll-behavior-x:contain}
|
| 125 |
+
.mountain-details{border-top:1px solid var(--line);font-size:13px}
|
| 126 |
+
.mountain-details summary{padding:12px 22px;cursor:pointer;color:var(--soft);font-weight:500}
|
| 127 |
+
.mountain-details summary:hover{background:var(--tint)}
|
| 128 |
+
.mountain-details summary:focus-visible{outline:2px solid var(--ink);outline-offset:-3px}
|
| 129 |
+
.mountain-table{width:100%;border-collapse:collapse;font-size:13px;font-variant-numeric:tabular-nums;text-align:left}
|
| 130 |
+
.mountain-table caption{padding:0 22px 8px;text-align:left;font-size:12px;color:var(--muted)}
|
| 131 |
+
.mountain-table th,.mountain-table td{padding:9px 12px;border-top:1px solid var(--line);vertical-align:top}
|
| 132 |
+
.mountain-table thead th{font-size:12px;font-weight:600;color:var(--soft);background:var(--tint)}
|
| 133 |
+
.mountain-table tbody th{font-weight:500}
|
| 134 |
+
.mountain-table tr>*:first-child{padding-left:22px;width:44px;color:var(--muted)}
|
| 135 |
+
.mountain-table tr>*:last-child{padding-right:22px;text-align:right}
|
| 136 |
+
.mountain:not(.mountain--ready) .mountain-canvas{display:none}
|
| 137 |
+
.mountain:not(.mountain--ready):not(.mountain--error):not(:has(.mountain-table)){display:none}
|
| 138 |
+
.mountain:not(.mountain--ready) .mountain-table caption{padding-top:12px}
|
| 139 |
+
.mountain--error .mountain-canvas::before{content:"Leaderboard data could not be read.";display:block;padding:18px 22px;font-size:13px;color:var(--muted)}
|
| 140 |
+
|
| 141 |
+
@media(prefers-reduced-motion:reduce){.mountain *{transition:none!important}}
|
| 142 |
+
@media(forced-colors:active){
|
| 143 |
+
.mountain-pill{border-color:ButtonText;forced-color-adjust:none;background:ButtonFace;color:ButtonText}
|
| 144 |
+
.mtn-marker-dot{stroke:CanvasText}
|
| 145 |
+
}
|
mountain.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Reused from the InferenceNet Challenge website; local adaptation adds Chinese labels.
|
| 2 |
+
/* Mountain-climb leaderboard: every entry is a climber whose altitude is its score,
|
| 3 |
+
* the flag marks the unconquered summit (score = max). ES module, zero dependencies.
|
| 4 |
+
*
|
| 5 |
+
* Markup:
|
| 6 |
+
* <figure class="mountain" data-mountain>
|
| 7 |
+
* <script type="application/json">{ "title": "…", "items": [ … ] }</script>
|
| 8 |
+
* <div class="mountain-canvas"></div>
|
| 9 |
+
* </figure>
|
| 10 |
+
* <script type="module" src="mountain.js"></script>
|
| 11 |
+
*
|
| 12 |
+
* The module mounts every [data-mountain] element once the DOM is ready; calling
|
| 13 |
+
* mountMountains() again (or mountain(el, data) twice) re-renders in place. */
|
| 14 |
+
|
| 15 |
+
const NS = "http://www.w3.org/2000/svg";
|
| 16 |
+
const DEFAULTS = {
|
| 17 |
+
compactBreakpoint: 700, // px; below this the pills move under the drawing
|
| 18 |
+
headingLevel: 3,
|
| 19 |
+
table: true, // emit a ranked <table class="mountain-table"> when the page has none
|
| 20 |
+
minGap: 28, // px; minimum centre distance between two markers
|
| 21 |
+
markerRadius: 11,
|
| 22 |
+
leaderRadius: 15,
|
| 23 |
+
aspect: 0.6, // drawing height / width (1200 × 720)
|
| 24 |
+
compactAspect: 0.75 // 4:3 on small screens
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
/* Terrain in unit coordinates: [x / width, altitude above the base line, 0..1 = summit]. */
|
| 28 |
+
const PEAK = [[-0.06, 0], [0.02, 0.06], [0.07, 0.17], [0.11, 0.14], [0.17, 0.29], [0.21, 0.26], [0.26, 0.42], [0.30, 0.39], [0.345, 0.55], [0.375, 0.53], [0.42, 0.73], [0.445, 0.71], [0.475, 0.88], [0.5, 1], [0.525, 0.87], [0.555, 0.84], [0.59, 0.72], [0.62, 0.69], [0.665, 0.52], [0.70, 0.47], [0.75, 0.34], [0.79, 0.30], [0.84, 0.18], [0.885, 0.15], [0.94, 0.06], [1.06, 0]];
|
| 29 |
+
const SNOW = [[0.42, 0.73], [0.445, 0.71], [0.475, 0.88], [0.5, 1], [0.525, 0.87], [0.555, 0.84], [0.59, 0.72], [0.60, 0.69], [0.592, 0.63], [0.578, 0.66], [0.567, 0.585], [0.552, 0.63], [0.541, 0.555], [0.531, 0.60], [0.52, 0.505], [0.506, 0.57], [0.495, 0.495], [0.484, 0.56], [0.471, 0.525], [0.461, 0.60], [0.451, 0.565], [0.441, 0.63], [0.431, 0.61], [0.425, 0.68]];
|
| 30 |
+
const SNOW_TONGUES = [
|
| 31 |
+
[[0.462, 0.605], [0.482, 0.575], [0.478, 0.46], [0.470, 0.43], [0.464, 0.47]],
|
| 32 |
+
[[0.512, 0.545], [0.531, 0.565], [0.535, 0.43], [0.526, 0.37], [0.519, 0.44]],
|
| 33 |
+
[[0.556, 0.61], [0.573, 0.585], [0.592, 0.50], [0.585, 0.465], [0.571, 0.53]],
|
| 34 |
+
[[0.388, 0.52], [0.404, 0.565], [0.416, 0.535], [0.412, 0.485], [0.398, 0.475]],
|
| 35 |
+
[[0.628, 0.60], [0.646, 0.585], [0.662, 0.525], [0.652, 0.50], [0.636, 0.535]]
|
| 36 |
+
];
|
| 37 |
+
const SHADE = [[0.5, 1], [0.515, 0.80], [0.53, 0.62], [0.56, 0.45], [0.585, 0.28], [0.62, 0.12], [0.66, 0], [1.06, 0], [0.94, 0.06], [0.885, 0.15], [0.84, 0.18], [0.79, 0.30], [0.75, 0.34], [0.70, 0.47], [0.665, 0.52], [0.62, 0.69], [0.59, 0.72], [0.555, 0.84], [0.525, 0.87]];
|
| 38 |
+
const STRIAE = [
|
| 39 |
+
[[0.36, 0.53], [0.33, 0.40], [0.29, 0.30], [0.23, 0.16]],
|
| 40 |
+
[[0.41, 0.66], [0.39, 0.50], [0.35, 0.36], [0.31, 0.20]],
|
| 41 |
+
[[0.66, 0.50], [0.69, 0.36], [0.74, 0.22], [0.80, 0.10]],
|
| 42 |
+
[[0.60, 0.66], [0.64, 0.48], [0.68, 0.30], [0.71, 0.14]]
|
| 43 |
+
];
|
| 44 |
+
const FAR = [[-0.06, 0], [0.0, 0.22], [0.05, 0.18], [0.10, 0.36], [0.15, 0.30], [0.20, 0.44], [0.25, 0.40], [0.31, 0.52], [0.36, 0.48], [0.42, 0.56], [0.5, 0.6], [0.58, 0.55], [0.64, 0.58], [0.70, 0.48], [0.75, 0.52], [0.80, 0.40], [0.86, 0.44], [0.92, 0.28], [0.97, 0.32], [1.06, 0.16], [1.06, 0]];
|
| 45 |
+
const MID = [[-0.06, 0], [0.0, 0.10], [0.06, 0.20], [0.10, 0.18], [0.16, 0.36], [0.20, 0.30], [0.24, 0.33], [0.29, 0.24], [0.34, 0.28], [0.38, 0.18], [0.42, 0.24], [0.47, 0.12], [0.53, 0.14], [0.58, 0.08], [0.63, 0.16], [0.68, 0.22], [0.72, 0.20], [0.77, 0.34], [0.82, 0.42], [0.86, 0.32], [0.90, 0.36], [0.95, 0.22], [1.0, 0.28], [1.06, 0.12], [1.06, 0]];
|
| 46 |
+
const MID_CAPS = [
|
| 47 |
+
[[0.135, 0.29], [0.16, 0.36], [0.185, 0.31], [0.175, 0.30], [0.16, 0.32], [0.148, 0.30]],
|
| 48 |
+
[[0.795, 0.37], [0.82, 0.42], [0.845, 0.36], [0.835, 0.35], [0.82, 0.37], [0.807, 0.355]]
|
| 49 |
+
];
|
| 50 |
+
const FOOT = [[-0.06, 0], [0.04, 0.05], [0.12, 0.03], [0.20, 0.075], [0.28, 0.04], [0.36, 0.06], [0.45, 0.02], [0.55, 0.05], [0.64, 0.03], [0.73, 0.08], [0.82, 0.045], [0.9, 0.07], [1.06, 0.03], [1.06, 0]];
|
| 51 |
+
const ROUTE = [[0.34, 0], [0.30, 0.10], [0.39, 0.20], [0.33, 0.33], [0.43, 0.44], [0.40, 0.56], [0.47, 0.68], [0.455, 0.80], [0.49, 0.92], [0.5, 1]];
|
| 52 |
+
/* Horizontal spread sequence for markers of one side, indexed by order within that side. */
|
| 53 |
+
const SPREAD = [0.45, 0.78, 0.22, 0.6, 0.92, 0.35, 0.7, 0.12, 0.52, 0.85, 0.3, 0.65];
|
| 54 |
+
|
| 55 |
+
let counter = 0;
|
| 56 |
+
const instances = new WeakMap();
|
| 57 |
+
|
| 58 |
+
/* ---------- small helpers ---------- */
|
| 59 |
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
| 60 |
+
const num = (v) => (Math.round(v * 10) / 10).toFixed(1).replace(/\.0$/, "");
|
| 61 |
+
const fmt = (v, unit) => {
|
| 62 |
+
const s = num(Number(v) || 0);
|
| 63 |
+
return unit === "%" ? `${s}%` : unit ? `${s} ${unit}` : s;
|
| 64 |
+
};
|
| 65 |
+
function h(tag, attrs, parent, text) {
|
| 66 |
+
const n = document.createElement(tag);
|
| 67 |
+
for (const k in attrs) if (attrs[k] != null) n.setAttribute(k, attrs[k]);
|
| 68 |
+
if (text != null) n.textContent = text;
|
| 69 |
+
if (parent) parent.appendChild(n);
|
| 70 |
+
return n;
|
| 71 |
+
}
|
| 72 |
+
function s(tag, attrs, parent, text) {
|
| 73 |
+
const n = document.createElementNS(NS, tag);
|
| 74 |
+
for (const k in attrs) if (attrs[k] != null) n.setAttribute(k, attrs[k]);
|
| 75 |
+
if (text != null) n.textContent = text;
|
| 76 |
+
if (parent) parent.appendChild(n);
|
| 77 |
+
return n;
|
| 78 |
+
}
|
| 79 |
+
const px = (v) => (Math.round(v * 10) / 10).toString();
|
| 80 |
+
|
| 81 |
+
function frame(W, compact, o) {
|
| 82 |
+
const aspect = compact ? o.compactAspect : W < 1000 ? Math.max(o.aspect, 0.7) : o.aspect;
|
| 83 |
+
const H = Math.round(W * aspect);
|
| 84 |
+
const base = H - (compact ? 26 : 30);
|
| 85 |
+
const summit = Math.round(H * (compact ? 0.27 : 0.13));
|
| 86 |
+
return { W, H, base, summit, range: base - summit, cx: W / 2, compact };
|
| 87 |
+
}
|
| 88 |
+
const pt = (f, p) => [p[0] * f.W, f.base - p[1] * f.range];
|
| 89 |
+
const pts = (f, list) => list.map((p) => pt(f, p).map(px).join(",")).join(" ");
|
| 90 |
+
|
| 91 |
+
/* Catmull-Rom spline through the points, as a cubic path. */
|
| 92 |
+
function smoothPath(list) {
|
| 93 |
+
let d = `M${px(list[0][0])},${px(list[0][1])}`;
|
| 94 |
+
for (let i = 0; i < list.length - 1; i++) {
|
| 95 |
+
const p0 = list[i - 1] || list[i], p1 = list[i], p2 = list[i + 1], p3 = list[i + 2] || p2;
|
| 96 |
+
const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
|
| 97 |
+
const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
|
| 98 |
+
d += ` C${px(c1[0])},${px(c1[1])} ${px(c2[0])},${px(c2[1])} ${px(p2[0])},${px(p2[1])}`;
|
| 99 |
+
}
|
| 100 |
+
return d;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/* Horizontal extent of a closed polygon (pixel coordinates) along the line y. */
|
| 104 |
+
function extentAt(poly, y) {
|
| 105 |
+
let lo = Infinity, hi = -Infinity;
|
| 106 |
+
for (let i = 0; i < poly.length; i++) {
|
| 107 |
+
const a = poly[i], b = poly[(i + 1) % poly.length];
|
| 108 |
+
if ((a[1] <= y && b[1] > y) || (b[1] <= y && a[1] > y)) {
|
| 109 |
+
const x = a[0] + ((y - a[1]) * (b[0] - a[0])) / (b[1] - a[1]);
|
| 110 |
+
lo = Math.min(lo, x); hi = Math.max(hi, x);
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
return lo <= hi ? [lo, hi] : null;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* Deterministic collision resolution: sweep x outward in 6px steps inside [lo, hi],
|
| 117 |
+
* then allow small vertical nudges; never places two markers closer than gap. */
|
| 118 |
+
function resolve(x, y, lo, hi, placed, gap) {
|
| 119 |
+
const free = (X, Y) => placed.every((p) => Math.hypot(p.x - X, p.y - Y) >= gap);
|
| 120 |
+
const dys = [0, -3, 3, -6, 6, -9, 9, -12, 12];
|
| 121 |
+
for (const dy of dys) {
|
| 122 |
+
for (let d = 0; d <= Math.max(hi - lo, 1) + 6; d += 6) {
|
| 123 |
+
for (const X of d === 0 ? [x] : [x + d, x - d]) {
|
| 124 |
+
if (X < lo - 0.5 || X > hi + 0.5) continue;
|
| 125 |
+
if (free(X, y + dy)) return { x: X, y: y + dy };
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
let best = { x, y }, score = -1; // last resort: the least crowded spot
|
| 130 |
+
for (let X = lo; X <= hi; X += 4) {
|
| 131 |
+
const m = Math.min(...placed.map((p) => Math.hypot(p.x - X, p.y - y)), Infinity);
|
| 132 |
+
if (m > score) { score = m; best = { x: X, y }; }
|
| 133 |
+
}
|
| 134 |
+
return best;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
function normalise(data) {
|
| 138 |
+
const d = Object.assign({ unit: "", max: 100, items: [], trails: [] }, data || {});
|
| 139 |
+
const max = Number(d.max) > 0 ? Number(d.max) : 100;
|
| 140 |
+
const items = (d.items || []).map((it) => ({
|
| 141 |
+
label: String(it.label ?? it.name ?? ""), value: Number(it.value) || 0,
|
| 142 |
+
group: it.group ?? "", kind: it.kind || "public", rank: it.rank
|
| 143 |
+
}));
|
| 144 |
+
items.sort((a, b) => (b.value - a.value) || String(a.label).localeCompare(String(b.label)));
|
| 145 |
+
items.forEach((it, i) => { if (!Number.isFinite(Number(it.rank))) it.rank = i + 1; });
|
| 146 |
+
items.sort((a, b) => a.rank - b.rank);
|
| 147 |
+
return { ...d, max, items, trails: Array.isArray(d.trails) ? d.trails : [] };
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/* ---------- component ---------- */
|
| 151 |
+
export function mountain(el, data, options = {}) {
|
| 152 |
+
let inst = instances.get(el);
|
| 153 |
+
if (inst) { inst.update(data, options); return inst; }
|
| 154 |
+
const o = Object.assign({}, DEFAULTS, options);
|
| 155 |
+
const id = el.id || `mountain-${++counter}`;
|
| 156 |
+
let d = normalise(data);
|
| 157 |
+
let canvas = el.querySelector(":scope > .mountain-canvas") || h("div", { class: "mountain-canvas" }, el);
|
| 158 |
+
let lastWidth = -1, raf = 0, ro = null;
|
| 159 |
+
el.classList.add("mountain");
|
| 160 |
+
|
| 161 |
+
function render() {
|
| 162 |
+
el.classList.add("mountain--ready"); // reveals the canvas before it is measured
|
| 163 |
+
const width = canvas.clientWidth;
|
| 164 |
+
if (!width) return; // hidden (closed tab, display:none): the ResizeObserver re-schedules when visible
|
| 165 |
+
lastWidth = width;
|
| 166 |
+
const compact = width < o.compactBreakpoint;
|
| 167 |
+
el.classList.toggle("mountain--compact", compact);
|
| 168 |
+
el.classList.toggle("mountain--narrow", !compact && width < 1000); // shorter pills keep the drawing wide
|
| 169 |
+
canvas.replaceChildren();
|
| 170 |
+
|
| 171 |
+
/* Header */
|
| 172 |
+
const head = h("div", { class: "mountain-head" }, canvas);
|
| 173 |
+
const headText = h("div", { class: "mountain-head-text" }, head);
|
| 174 |
+
if (d.title) h(`h${o.headingLevel}`, { class: "mountain-title" }, headText, d.title);
|
| 175 |
+
if (d.subtitle) h("p", { class: "mountain-subtitle" }, headText, d.subtitle);
|
| 176 |
+
if (compact && d.date) h("p", { class: "mountain-stamp" }, head, d.date);
|
| 177 |
+
|
| 178 |
+
/* Stage: drawing + pills */
|
| 179 |
+
const stage = h("div", { class: "mountain-stage" }, canvas);
|
| 180 |
+
/* Trails need vertical room on phones: a taller compact drawing keeps labels off the markers. */
|
| 181 |
+
const f = frame(width, compact, d.trails.length ? { ...o, compactAspect: Math.max(o.compactAspect, 0.9) } : o);
|
| 182 |
+
const svg = s("svg", {
|
| 183 |
+
class: "mountain-svg", viewBox: `0 0 ${f.W} ${f.H}`, width: f.W, height: f.H,
|
| 184 |
+
role: "img", "aria-labelledby": `${id}-title ${id}-desc`, focusable: "false"
|
| 185 |
+
}, stage);
|
| 186 |
+
const leader = d.items[0];
|
| 187 |
+
const isZh = d.locale === "zh";
|
| 188 |
+
const n = d.items.length;
|
| 189 |
+
s("title", { id: `${id}-title` }, svg, `${d.title || "Leaderboard"}: ${n} entries climbing by ${d.metric || "score"}.`);
|
| 190 |
+
s("desc", { id: `${id}-desc` }, svg, leader
|
| 191 |
+
? `Altitude is ${d.metric || "score"}; the summit flag marks ${fmt(d.max, d.unit)}. Leader: ${leader.label} at ${fmt(leader.value, d.unit)}. Full ranking in the table below.`
|
| 192 |
+
: "No entries yet.");
|
| 193 |
+
svg.setAttribute("aria-label", `${d.title || "Leaderboard"}: ${n} entries climbing by ${d.metric || "score"}${leader ? `; leader ${leader.label} at ${fmt(leader.value, d.unit)}` : ""}.`);
|
| 194 |
+
|
| 195 |
+
const pills = h("ol", { class: "mountain-pills", "aria-label": `Ranked entries by ${d.metric || "score"}` }, stage);
|
| 196 |
+
const pillOf = new Map();
|
| 197 |
+
d.items.forEach((it) => {
|
| 198 |
+
const right = it.rank % 2 === 1;
|
| 199 |
+
const li = h("li", {
|
| 200 |
+
class: `mountain-pill mountain-pill--${right ? "right" : "left"}${it.kind === "internal" ? " mountain-pill--internal" : ""}`,
|
| 201 |
+
"data-rank": it.rank, "aria-describedby": `${id}-d${it.rank}`
|
| 202 |
+
}, pills);
|
| 203 |
+
const badge = h("span", { class: `mountain-badge${it.rank <= 3 ? ` mountain-badge--${["gold", "silver", "bronze"][it.rank - 1]}` : ""}`, "aria-hidden": "true" }, li, String(it.rank));
|
| 204 |
+
badge.dataset.rank = it.rank;
|
| 205 |
+
h("span", { class: "mountain-vh" }, li, `Rank ${it.rank}: `);
|
| 206 |
+
h("span", { class: "mountain-pill-label" }, li, it.label);
|
| 207 |
+
h("span", { class: "mountain-pill-value" }, li, fmt(it.value, d.unit));
|
| 208 |
+
pillOf.set(it.rank, li);
|
| 209 |
+
});
|
| 210 |
+
|
| 211 |
+
/* Column geometry from the measured pills (desktop only). */
|
| 212 |
+
const gutter = 16;
|
| 213 |
+
let leftEdge = 0, rightEdge = f.W, maxL = 0, maxR = 0, nL = 0, nR = 0;
|
| 214 |
+
if (!compact) {
|
| 215 |
+
pillOf.forEach((li, rank) => {
|
| 216 |
+
const w = li.offsetWidth;
|
| 217 |
+
if (rank % 2 === 1) { maxR = Math.max(maxR, w); nR++; } else { maxL = Math.max(maxL, w); nL++; }
|
| 218 |
+
});
|
| 219 |
+
leftEdge = gutter + maxL; rightEdge = f.W - gutter - maxR;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* Marker placement. */
|
| 223 |
+
const peakPx = PEAK.map((p) => pt(f, p));
|
| 224 |
+
const r0 = compact ? Math.max(9, o.markerRadius - 2) : o.markerRadius;
|
| 225 |
+
const rLead = compact ? Math.max(12, o.leaderRadius - 2) : o.leaderRadius;
|
| 226 |
+
const placed = [];
|
| 227 |
+
const byLabel = new Map();
|
| 228 |
+
const trailTo = new Map(d.trails.map((t) => [t.from, t.to]));
|
| 229 |
+
const sideCount = { 1: 0, "-1": 0 };
|
| 230 |
+
d.items.forEach((it) => {
|
| 231 |
+
const side = it.rank % 2 === 1 ? 1 : -1;
|
| 232 |
+
const k = sideCount[side]++;
|
| 233 |
+
const r = it.rank === 1 ? rLead : r0;
|
| 234 |
+
const y = Math.max(f.summit + 1, f.base - clamp(it.value / d.max, 0, 1) * f.range);
|
| 235 |
+
const ext = extentAt(peakPx, y) || [f.cx - r, f.cx + r];
|
| 236 |
+
let lo, hi;
|
| 237 |
+
if (side > 0) { lo = Math.max(f.cx + 0.03 * f.W, ext[0] + r + 4); hi = Math.min(ext[1] - r - 4, rightEdge - r - 14, f.W - r - 6); }
|
| 238 |
+
else { hi = Math.min(f.cx - 0.03 * f.W, ext[1] - r - 4); lo = Math.max(ext[0] + r + 4, leftEdge + r + 14, r + 6); }
|
| 239 |
+
if (lo > hi) { const m = clamp((lo + hi) / 2, leftEdge + r + 14, rightEdge - r - 14); lo = hi = m; }
|
| 240 |
+
let x = lo + SPREAD[k % SPREAD.length] * (hi - lo);
|
| 241 |
+
const to = trailTo.get(it.label);
|
| 242 |
+
if (to && byLabel.has(to)) x = clamp(byLabel.get(to).x + side * 0.07 * f.W, lo, hi);
|
| 243 |
+
const p = resolve(x, y, lo, hi, placed, o.minGap);
|
| 244 |
+
const m = { ...p, r, item: it, side };
|
| 245 |
+
placed.push(m); byLabel.set(it.label, m);
|
| 246 |
+
});
|
| 247 |
+
/* Trail endpoints that are not ranked entries become unlabeled waypoints. */
|
| 248 |
+
d.trails.forEach((t) => {
|
| 249 |
+
[["from", t.fromValue], ["to", t.toValue]].forEach(([key, v]) => {
|
| 250 |
+
const label = t[key];
|
| 251 |
+
if (byLabel.has(label) || !Number.isFinite(Number(v))) return;
|
| 252 |
+
const other = byLabel.get(t[key === "from" ? "to" : "from"]);
|
| 253 |
+
const side = other ? other.side : 1;
|
| 254 |
+
const y = Math.max(f.summit + 1, f.base - clamp(Number(v) / d.max, 0, 1) * f.range);
|
| 255 |
+
const ext = extentAt(peakPx, y) || [f.cx - r0, f.cx + r0];
|
| 256 |
+
const lo = Math.max(ext[0] + r0 + 4, leftEdge + r0 + 14), hi = Math.min(ext[1] - r0 - 4, rightEdge - r0 - 14);
|
| 257 |
+
const x0 = other ? clamp(other.x + side * 0.08 * f.W, lo, hi) : f.cx;
|
| 258 |
+
const p = resolve(x0, y, Math.min(lo, hi), Math.max(lo, hi), placed, o.minGap);
|
| 259 |
+
const m = { ...p, r: r0 - 2, item: { label, value: Number(v), kind: t.kind || "internal", rank: null }, side, synthetic: true };
|
| 260 |
+
placed.push(m); byLabel.set(label, m);
|
| 261 |
+
});
|
| 262 |
+
});
|
| 263 |
+
|
| 264 |
+
/* Pill positions: bottom-aligned stacks on both sides, ranks ascending downwards. */
|
| 265 |
+
const pillH = 28;
|
| 266 |
+
const cols = { 1: [], "-1": [] };
|
| 267 |
+
if (!compact) {
|
| 268 |
+
const nSide = Math.max(nL, nR, 1);
|
| 269 |
+
const bottom = f.base - 6;
|
| 270 |
+
const pitch = clamp(Math.floor((bottom - f.H * 0.38) / nSide), 30, 34);
|
| 271 |
+
const top = bottom - nSide * pitch;
|
| 272 |
+
const idx = { 1: 0, "-1": 0 };
|
| 273 |
+
d.items.forEach((it) => {
|
| 274 |
+
const side = it.rank % 2 === 1 ? 1 : -1;
|
| 275 |
+
const li = pillOf.get(it.rank);
|
| 276 |
+
const w = li.offsetWidth;
|
| 277 |
+
const yTop = top + idx[side]++ * pitch + (pitch - pillH) / 2;
|
| 278 |
+
li.style.top = `${px(yTop)}px`;
|
| 279 |
+
li.style.left = side > 0 ? `${px(f.W - gutter - w)}px` : `${px(gutter)}px`;
|
| 280 |
+
cols[side].push({ rank: it.rank, cy: yTop + pillH / 2, inner: side > 0 ? f.W - gutter - w - 3 : gutter + w + 3, top: yTop, bottom: yTop + pillH });
|
| 281 |
+
});
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
/* ---------- drawing ---------- */
|
| 285 |
+
const defs = s("defs", {}, svg);
|
| 286 |
+
const sky = s("linearGradient", { id: `${id}-sky`, x1: "0", y1: "0", x2: "0", y2: "1" }, defs);
|
| 287 |
+
s("stop", { offset: "0", class: "mtn-sky-top" }, sky);
|
| 288 |
+
s("stop", { offset: "1", class: "mtn-sky-low" }, sky);
|
| 289 |
+
const haze = s("linearGradient", { id: `${id}-haze`, x1: "0", y1: "0", x2: "0", y2: "1" }, defs);
|
| 290 |
+
s("stop", { offset: "0", class: "mtn-haze-top" }, haze);
|
| 291 |
+
s("stop", { offset: "1", class: "mtn-haze-low" }, haze);
|
| 292 |
+
|
| 293 |
+
s("rect", { class: "mtn-sky", x: 0, y: 0, width: f.W, height: f.base, fill: `url(#${id}-sky)` }, svg);
|
| 294 |
+
const scene = s("g", { class: "mtn-scene", "aria-hidden": "true" }, svg);
|
| 295 |
+
s("polygon", { class: "mtn-far", points: pts(f, FAR) }, scene);
|
| 296 |
+
s("polygon", { class: "mtn-mid", points: pts(f, MID) }, scene);
|
| 297 |
+
MID_CAPS.forEach((c) => s("polygon", { class: "mtn-cap", points: pts(f, c) }, scene));
|
| 298 |
+
s("rect", { class: "mtn-haze", x: 0, y: f.base - f.range * 0.42, width: f.W, height: f.range * 0.42 + 1, fill: `url(#${id}-haze)` }, scene);
|
| 299 |
+
s("polygon", { class: "mtn-near", points: pts(f, PEAK) }, scene);
|
| 300 |
+
s("polygon", { class: "mtn-snow", points: pts(f, SNOW) }, scene);
|
| 301 |
+
SNOW_TONGUES.forEach((t) => s("polygon", { class: "mtn-snow", points: pts(f, t) }, scene));
|
| 302 |
+
s("polygon", { class: "mtn-shade", points: pts(f, SHADE) }, scene);
|
| 303 |
+
STRIAE.forEach((l) => s("polyline", { class: "mtn-stria", points: pts(f, l) }, scene));
|
| 304 |
+
s("polygon", { class: "mtn-foot", points: pts(f, FOOT) }, scene);
|
| 305 |
+
s("polygon", { class: "mtn-outline", points: pts(f, PEAK) }, scene);
|
| 306 |
+
s("polygon", { class: "mtn-outline mtn-outline--foot", points: pts(f, FOOT) }, scene);
|
| 307 |
+
|
| 308 |
+
/* Contour lines every 25 % with a label where the pill columns leave room. */
|
| 309 |
+
const contours = s("g", { class: "mtn-contours" }, scene);
|
| 310 |
+
const labelFont = 12;
|
| 311 |
+
[0.25, 0.5, 0.75].forEach((a) => {
|
| 312 |
+
const y = f.base - a * f.range;
|
| 313 |
+
s("line", { class: "mtn-contour", x1: 0, y1: px(y), x2: f.W, y2: px(y) }, contours);
|
| 314 |
+
const text = `${num(a * d.max)}${d.unit === "%" ? "%" : ""}`;
|
| 315 |
+
const collides = (col) => col.some((c) => y - labelFont - 4 < c.bottom && y + 2 > c.top - 4);
|
| 316 |
+
if (!collides(cols[1])) s("text", { class: "mtn-contour-label", x: f.W - 10, y: px(y - 5), "text-anchor": "end" }, contours, text);
|
| 317 |
+
else if (!collides(cols["-1"])) s("text", { class: "mtn-contour-label", x: 10, y: px(y - 5) }, contours, text);
|
| 318 |
+
});
|
| 319 |
+
s("line", { class: "mtn-base", x1: 0, y1: f.base + 0.5, x2: f.W, y2: f.base + 0.5 }, scene);
|
| 320 |
+
s("text", { class: "mtn-contour-label", x: f.W - 10, y: f.base + 16, "text-anchor": "end" }, scene, `0${d.unit === "%" ? "%" : ""} · ${isZh ? "起点" : "base camp"}`);
|
| 321 |
+
|
| 322 |
+
/* Route and flag. */
|
| 323 |
+
s("path", { class: "mtn-route", d: smoothPath(ROUTE.map((p) => pt(f, p))), fill: "none" }, scene);
|
| 324 |
+
const top = pt(f, [0.5, 1]);
|
| 325 |
+
const poleH = compact ? Math.min(34, Math.max(26, f.summit - 30)) : Math.max(34, f.H * 0.075);
|
| 326 |
+
s("line", { class: "mtn-pole", x1: px(top[0]), y1: px(top[1]), x2: px(top[0]), y2: px(top[1] - poleH) }, scene);
|
| 327 |
+
const fw = Math.max(22, f.W * 0.028), fh = fw * 0.62, fx = top[0] + 1, fy = top[1] - poleH;
|
| 328 |
+
s("path", { class: "mtn-flag", d: `M${px(fx)},${px(fy)} Q${px(fx + fw * 0.55)},${px(fy + fh * 0.2)} ${px(fx + fw)},${px(fy + fh * 0.5)} Q${px(fx + fw * 0.55)},${px(fy + fh * 0.8)} ${px(fx)},${px(fy + fh)} Z` }, scene);
|
| 329 |
+
if (d.peakLabel) {
|
| 330 |
+
if (compact) s("text", { class: "mtn-peak-label", x: px(f.cx), y: 18, "text-anchor": "middle" }, scene, d.peakLabel);
|
| 331 |
+
else s("text", { class: "mtn-peak-label", x: px(fx + fw + 10), y: px(fy + 12) }, scene, d.peakLabel);
|
| 332 |
+
}
|
| 333 |
+
if (!compact && d.date) s("text", { class: "mtn-stamp", x: f.W - 16, y: 26, "text-anchor": "end" }, scene, d.date);
|
| 334 |
+
|
| 335 |
+
/* Connectors (pill → marker); straight, from the pill's inner edge, so they never cross a pill. */
|
| 336 |
+
const connectors = s("g", { class: "mtn-connectors" }, scene);
|
| 337 |
+
const connOf = new Map();
|
| 338 |
+
if (!compact) {
|
| 339 |
+
[1, -1].forEach((side) => cols[side].forEach((c) => {
|
| 340 |
+
const m = placed.find((p) => p.item.rank === c.rank);
|
| 341 |
+
if (!m) return;
|
| 342 |
+
const dx = m.x - c.inner, dy = m.y - c.cy, len = Math.hypot(dx, dy) || 1;
|
| 343 |
+
const ex = m.x - (dx / len) * (m.r + 3), ey = m.y - (dy / len) * (m.r + 3);
|
| 344 |
+
connOf.set(c.rank, s("line", { class: "mtn-connector", x1: px(c.inner), y1: px(c.cy), x2: px(ex), y2: px(ey) }, connectors));
|
| 345 |
+
}));
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
/* Trails: dashed climbing arrow from the baseline marker to the improved one. */
|
| 349 |
+
const trails = s("g", { class: "mtn-trails" }, scene);
|
| 350 |
+
d.trails.forEach((t) => {
|
| 351 |
+
const a = byLabel.get(t.from), b = byLabel.get(t.to);
|
| 352 |
+
if (!a || !b) return;
|
| 353 |
+
const dx = b.x - a.x, dy = b.y - a.y, len = Math.hypot(dx, dy) || 1;
|
| 354 |
+
let nx = -dy / len, ny = dx / len;
|
| 355 |
+
const mid = [(a.x + b.x) / 2, (a.y + b.y) / 2];
|
| 356 |
+
const bow = Math.min(0.22 * len, 60);
|
| 357 |
+
/* Bow (and label) on the outer side of the trail: for a steep trail that is the side away from the summit line. */
|
| 358 |
+
const side = a.side || 1;
|
| 359 |
+
const inward = Math.abs(nx) >= 0.45 ? nx * side < 0 : ((mid[0] + nx * bow) - f.cx) * side < 0;
|
| 360 |
+
if (inward) { nx = -nx; ny = -ny; }
|
| 361 |
+
const c = [mid[0] + nx * bow, mid[1] + ny * bow];
|
| 362 |
+
const sa = [c[0] - a.x, c[1] - a.y], la = Math.hypot(...sa) || 1;
|
| 363 |
+
const sb = [c[0] - b.x, c[1] - b.y], lb = Math.hypot(...sb) || 1;
|
| 364 |
+
const p0 = [a.x + (sa[0] / la) * (a.r + 5), a.y + (sa[1] / la) * (a.r + 5)];
|
| 365 |
+
const p1 = [b.x + (sb[0] / lb) * (b.r + 3.5), b.y + (sb[1] / lb) * (b.r + 3.5)]; // arrowhead touches the ring
|
| 366 |
+
const g = s("g", { class: "mtn-trail" }, trails);
|
| 367 |
+
s("path", { class: "mtn-trail-path", d: `M${px(p0[0])},${px(p0[1])} Q${px(c[0])},${px(c[1])} ${px(p1[0])},${px(p1[1])}`, fill: "none" }, g);
|
| 368 |
+
const tx = p1[0] - c[0], ty = p1[1] - c[1], tl = Math.hypot(tx, ty) || 1, ux = tx / tl, uy = ty / tl;
|
| 369 |
+
const tip = p1, back = [tip[0] - ux * 9, tip[1] - uy * 9];
|
| 370 |
+
s("polygon", { class: "mtn-trail-head", points: `${px(tip[0])},${px(tip[1])} ${px(back[0] - uy * 4)},${px(back[1] + ux * 4)} ${px(back[0] + uy * 4)},${px(back[1] - ux * 4)}` }, g);
|
| 371 |
+
if (t.label) {
|
| 372 |
+
const q = [0.25 * p0[0] + 0.5 * c[0] + 0.25 * p1[0], 0.25 * p0[1] + 0.5 * c[1] + 0.25 * p1[1]];
|
| 373 |
+
/* On a steep trail the label hangs off its outer side (anchored at the inner edge) so it never crosses back over the trail or the markers. */
|
| 374 |
+
const steep = Math.abs(nx) >= 0.45, off = steep ? 9 : 14;
|
| 375 |
+
s("text", { class: "mtn-trail-label", x: px(q[0] + nx * off), y: px(q[1] + ny * off + 4), "text-anchor": steep ? (nx > 0 ? "start" : "end") : "middle" }, g, t.label);
|
| 376 |
+
}
|
| 377 |
+
});
|
| 378 |
+
|
| 379 |
+
/* Markers. Drawn last so they sit above route, trails and snow. */
|
| 380 |
+
const markers = s("g", { class: "mtn-markers" }, svg);
|
| 381 |
+
const markerOf = new Map();
|
| 382 |
+
placed.forEach((m) => {
|
| 383 |
+
const it = m.item;
|
| 384 |
+
const cls = ["mtn-marker"];
|
| 385 |
+
if (it.rank === 1) cls.push("mtn-marker--leader");
|
| 386 |
+
if (it.rank === 2) cls.push("mtn-marker--silver");
|
| 387 |
+
if (it.rank === 3) cls.push("mtn-marker--bronze");
|
| 388 |
+
if (it.kind === "internal") cls.push("mtn-marker--internal");
|
| 389 |
+
if (m.synthetic) cls.push("mtn-marker--waypoint");
|
| 390 |
+
const g = s("g", { class: cls.join(" "), transform: `translate(${px(m.x)} ${px(m.y)})` }, markers);
|
| 391 |
+
s("circle", { class: "mtn-marker-halo", r: m.r + 6 }, g);
|
| 392 |
+
if (it.kind === "internal") s("circle", { class: "mtn-marker-ring", r: m.r + 4.5 }, g);
|
| 393 |
+
s("circle", { class: "mtn-marker-dot", r: m.r }, g);
|
| 394 |
+
if (!m.synthetic) {
|
| 395 |
+
s("text", { class: "mtn-marker-num", "text-anchor": "middle", dy: "0.36em" }, g, String(it.rank));
|
| 396 |
+
s("text", { class: "mtn-marker-val", "text-anchor": "middle", y: -(m.r + 8) }, g, fmt(it.value, d.unit));
|
| 397 |
+
s("desc", { id: `${id}-d${it.rank}` }, g, `${fmt(it.value, d.unit)} ${d.metric || ""}; rank ${it.rank} of ${n}${it.group ? `; ${it.group}` : ""}${it.kind === "internal" ? "; internal study" : ""}.`);
|
| 398 |
+
markerOf.set(it.rank, g);
|
| 399 |
+
} else {
|
| 400 |
+
s("title", {}, g, `${it.label}: ${fmt(it.value, d.unit)}`);
|
| 401 |
+
}
|
| 402 |
+
});
|
| 403 |
+
|
| 404 |
+
/* Hover / focus linking between pills and markers. */
|
| 405 |
+
pillOf.forEach((li, rank) => {
|
| 406 |
+
const g = markerOf.get(rank), line = connOf.get(rank);
|
| 407 |
+
const set = (on) => { li.classList.toggle("is-active", on); g && g.classList.toggle("is-active", on); line && line.classList.toggle("is-active", on); };
|
| 408 |
+
li.addEventListener("mouseenter", () => set(true));
|
| 409 |
+
li.addEventListener("mouseleave", () => set(false));
|
| 410 |
+
if (g) { g.addEventListener("mouseenter", () => set(true)); g.addEventListener("mouseleave", () => set(false)); }
|
| 411 |
+
});
|
| 412 |
+
|
| 413 |
+
/* Caption + legend. */
|
| 414 |
+
const caption = h("div", { class: "mountain-caption" }, canvas);
|
| 415 |
+
const noteParts = [];
|
| 416 |
+
if (d.metric) noteParts.push(isZh ? `高度 = ${d.metric}${d.unit ? `(${d.unit})` : ""};顶峰 = ${fmt(d.max, d.unit)}` : `Altitude = ${d.metric}${d.unit ? ` (${d.unit})` : ""}; summit = ${fmt(d.max, d.unit)}`);
|
| 417 |
+
if (d.date) noteParts.push(d.date);
|
| 418 |
+
if (d.caption) noteParts.push(d.caption);
|
| 419 |
+
h("p", { class: "mountain-note" }, caption, noteParts.join(" · "));
|
| 420 |
+
const legend = h("ul", { class: "mountain-legend", "aria-label": "Legend" }, caption);
|
| 421 |
+
const key = (cls, text) => { const li = h("li", {}, legend); h("i", { class: `mtn-key ${cls}`, "aria-hidden": "true" }, li); li.appendChild(document.createTextNode(text)); };
|
| 422 |
+
if (n) key("mtn-key--gold", "rank 1");
|
| 423 |
+
if (d.items.some((i) => i.kind !== "internal")) key("mtn-key--public", "public result");
|
| 424 |
+
if (d.items.some((i) => i.kind === "internal") || d.trails.length) key("mtn-key--internal", "internal study");
|
| 425 |
+
if (d.trails.length) key("mtn-key--trail", d.trailLegend || "gain with harness");
|
| 426 |
+
|
| 427 |
+
/* Ranked table for no-JS and screen readers: keep the page's own, or emit one. */
|
| 428 |
+
let table = el.querySelector(":scope > .mountain-table, :scope > .mountain-details .mountain-table, :scope > .mountain-table-wrap .mountain-table");
|
| 429 |
+
if (!table && o.table) {
|
| 430 |
+
table = h("table", { class: "mountain-table" });
|
| 431 |
+
h("caption", {}, table, `${d.title || "Leaderboard"} — ranked by ${d.metric || "score"}${d.date ? ` (${d.date})` : ""}.`);
|
| 432 |
+
const tr = h("tr", {}, h("thead", {}, table));
|
| 433 |
+
h("th", { scope: "col" }, tr, "#");
|
| 434 |
+
h("th", { scope: "col" }, tr, "Entry");
|
| 435 |
+
if (d.items.some((i) => i.group)) h("th", { scope: "col" }, tr, "Group");
|
| 436 |
+
h("th", { scope: "col" }, tr, d.metric || "Score");
|
| 437 |
+
const tb = h("tbody", {}, table);
|
| 438 |
+
d.items.forEach((it) => {
|
| 439 |
+
const row = h("tr", {}, tb);
|
| 440 |
+
h("td", {}, row, String(it.rank));
|
| 441 |
+
h("th", { scope: "row" }, row, it.label + (it.kind === "internal" ? " (internal study)" : ""));
|
| 442 |
+
if (d.items.some((i) => i.group)) h("td", {}, row, it.group);
|
| 443 |
+
h("td", {}, row, fmt(it.value, d.unit));
|
| 444 |
+
});
|
| 445 |
+
el.appendChild(table);
|
| 446 |
+
}
|
| 447 |
+
if (table && !table.closest(".mountain-details")) {
|
| 448 |
+
const details = h("details", { class: "mountain-details" });
|
| 449 |
+
h("summary", {}, details, `Ranked list · ${n} ${n === 1 ? "entry" : "entries"}`);
|
| 450 |
+
table.replaceWith(details);
|
| 451 |
+
details.appendChild(table);
|
| 452 |
+
}
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
function schedule() {
|
| 456 |
+
cancelAnimationFrame(raf);
|
| 457 |
+
raf = requestAnimationFrame(() => { if (canvas.clientWidth !== lastWidth) render(); });
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
inst = {
|
| 461 |
+
el, render,
|
| 462 |
+
update(next, opts) {
|
| 463 |
+
if (next) d = normalise(next);
|
| 464 |
+
if (opts) Object.assign(o, opts);
|
| 465 |
+
lastWidth = -1; render();
|
| 466 |
+
},
|
| 467 |
+
destroy() {
|
| 468 |
+
ro && ro.disconnect(); cancelAnimationFrame(raf);
|
| 469 |
+
instances.delete(el); el.classList.remove("mountain--ready", "mountain--compact", "mountain--narrow");
|
| 470 |
+
}
|
| 471 |
+
};
|
| 472 |
+
instances.set(el, inst);
|
| 473 |
+
render();
|
| 474 |
+
if (typeof ResizeObserver === "function") { ro = new ResizeObserver(schedule); ro.observe(canvas); }
|
| 475 |
+
else window.addEventListener("resize", schedule);
|
| 476 |
+
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => { lastWidth = -1; schedule(); }).catch(() => {});
|
| 477 |
+
return inst;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
export function mountMountains(root = document) {
|
| 481 |
+
const out = [];
|
| 482 |
+
root.querySelectorAll("[data-mountain]").forEach((el) => {
|
| 483 |
+
const script = el.querySelector(':scope > script[type="application/json"]');
|
| 484 |
+
let data = null;
|
| 485 |
+
try { data = script ? JSON.parse(script.textContent) : null; }
|
| 486 |
+
catch (err) { el.classList.add("mountain--error"); return; }
|
| 487 |
+
if (!data) return;
|
| 488 |
+
const opts = {};
|
| 489 |
+
if (el.dataset.headingLevel) opts.headingLevel = Number(el.dataset.headingLevel);
|
| 490 |
+
if (el.dataset.compactBreakpoint) opts.compactBreakpoint = Number(el.dataset.compactBreakpoint);
|
| 491 |
+
out.push(mountain(el, data, opts));
|
| 492 |
+
});
|
| 493 |
+
return out;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
if (typeof document !== "undefined") {
|
| 497 |
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => mountMountains(), { once: true });
|
| 498 |
+
else mountMountains();
|
| 499 |
+
}
|