skyan1002's picture
Reframe routing as ONE topic on two axes (method x space); add unifying experiment: emergent GIUH, what space buys, no unique UH for nonlinear KW
823ca50 verified
Raw
History Blame Contribute Delete
5.1 kB
/* shared.js -- common runtime for all module pages:
top navigation, Pyodide bootstrap, editable-code runner, plot helpers. */
const NAV_ITEMS = [
["index.html", "导航 Home"],
["m00_overview.html", "0 水量平衡"],
["m01_forcing.html", "1 Forcing"],
["m02_pet.html", "2 PET"],
["m05_runoff.html", "5 产流"],
["m06_response.html", "6 Response"],
["m07_routing.html", "7 Routing·方法"],
["m08_channel.html", "8a Routing·空间"],
];
function renderNav(here) {
const nav = document.createElement("div");
nav.className = "topnav";
const inner = document.createElement("div");
inner.className = "wrap";
const brand = document.createElement("span");
brand.className = "brand";
brand.textContent = "💧 HydroModel Builder";
inner.appendChild(brand);
for (const [href, label] of NAV_ITEMS) {
// skip links to pages that don't exist yet (marked with data-disabled below)
const a = document.createElement("a");
a.href = href;
a.textContent = label;
if (here === href) a.className = "here";
inner.appendChild(a);
}
nav.appendChild(inner);
document.body.prepend(nav);
}
/* ---------- Pyodide bootstrap (one shared promise) ---------- */
let _pyodidePromise = null;
function getPyodide(statusEl) {
if (!_pyodidePromise) {
if (statusEl) statusEl.textContent = "正在加载 Python 运行时 (~10s 首次) …";
_pyodidePromise = loadPyodide().then(async (py) => {
await py.loadPackage("numpy");
if (statusEl) statusEl.textContent = "Python (numpy) 就绪 — 可改代码后重跑";
return py;
});
}
return _pyodidePromise;
}
/* ---------- Editable-code runner ----------
makeRunner({codeEl, runBtn, resetBtn, statusEl, errEl, buildScript, onResult})
- codeEl: <textarea> holding user-editable Python
- buildScript(userCode, params) -> full python source to exec; must set __out
- onResult(jsValue, params): update plots/readouts
- params(): collect current slider values */
function makeRunner(cfg) {
const defaultCode = cfg.codeEl.value;
async function run() {
const py = await getPyodide(cfg.statusEl);
if (cfg.errEl) cfg.errEl.innerHTML = "";
cfg.runBtn.disabled = true;
try {
const params = cfg.params ? cfg.params() : {};
const src = cfg.buildScript(cfg.codeEl.value, params);
const t0 = performance.now();
py.runPython(src);
const ms = performance.now() - t0;
const out = py.globals.get("__out");
const js = out.toJs({ dict_converter: Object.fromEntries });
if (out.destroy) out.destroy();
cfg.onResult(js, params, ms);
} catch (e) {
if (cfg.errEl)
cfg.errEl.innerHTML =
'<div class="err">运行出错 / error:\n' + (e && e.message ? e.message : e) + "</div>";
} finally {
cfg.runBtn.disabled = false;
}
}
cfg.runBtn.addEventListener("click", run);
if (cfg.resetBtn)
cfg.resetBtn.addEventListener("click", () => {
cfg.codeEl.value = defaultCode;
run();
});
if (cfg.autorun !== false) getPyodide(cfg.statusEl).then(run);
return run;
}
/* ---------- slider helper: live label + rerun on release ---------- */
function bindSlider(id, labelId, fmt, onchange) {
const el = document.getElementById(id);
const lab = document.getElementById(labelId);
const f = fmt || ((v) => v);
const upd = () => (lab.textContent = f(parseFloat(el.value)));
el.addEventListener("input", upd);
if (onchange) el.addEventListener("change", onchange);
upd();
return el;
}
/* ---------- plot helpers ---------- */
const PLOT_CFG = { responsive: true, displayModeBar: false };
function linePlot(div, traces, ylab, xlab, extra) {
Plotly.react(
div,
traces,
Object.assign(
{
margin: { t: 10, r: 10, b: 40, l: 55 },
legend: { orientation: "h", y: 1.13 },
xaxis: { title: xlab || "day" },
yaxis: { title: ylab || "" },
},
extra || {}
),
PLOT_CFG
);
}
/* version tabs: makeTabs(containerId, [{key,label,crest}], onSwitch) */
function makeTabs(containerId, versions, onSwitch) {
const box = document.getElementById(containerId);
box.className = "vtabs";
let current = versions[0].key;
for (const v of versions) {
const b = document.createElement("button");
b.innerHTML = v.label + (v.crest ? '<span class="crest-mini">CREST</span>' : "");
b.dataset.key = v.key;
if (v.key === current) b.className = "on";
b.addEventListener("click", () => {
current = v.key;
for (const c of box.children) c.className = c.dataset.key === current ? "on" : "";
onSwitch(current);
});
box.appendChild(b);
}
return () => current;
}
function footer() {
const f = document.createElement("div");
f.className = "foot";
f.innerHTML =
'Made with Claude Code · hydrologic-model-builder skill · data: TU Delft CIE4431 Hesperange · anchor: <a href="https://github.com/HyDROSLab/EF5">CREST/EF5</a>';
document.querySelector(".wrap:last-of-type")?.appendChild(f) || document.body.appendChild(f);
}