Download shared.js from skyan1002/hydrologic-model-builder: direct link, hf CLI and curl.
- Browser
- Download file 5.1 kB
-
https://huggingface.co/spaces/skyan1002/hydrologic-model-builder/resolve/main/shared.js
- Command line
-
hf download hf://spaces/skyan1002/hydrologic-model-builder/shared.js
-
curl -L -o shared.js https://huggingface.co/spaces/skyan1002/hydrologic-model-builder/resolve/main/shared.js
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); | |
| } | |