- Update all submodule URLs from gitea.centraal.wbd-rd.nl to gitea.wbd-rd.nl - Add settler as proper submodule in .gitmodules - Add agent skills, function anchors, decisions, and improvements - Add Docker configuration and scripts - Add manuals and third_party docs - Update .gitignore with secrets and build artifacts - Remove stale .tgz build artifact Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
811 lines
42 KiB
HTML
811 lines
42 KiB
HTML
<!doctype html>
|
|
<html lang="en" data-theme="light">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>EVOLV RotatingMachine Anchor</title>
|
|
<style>
|
|
:root {
|
|
--bg: #f3f6fb;
|
|
--bg-grad: radial-gradient(circle at 0% 0%, #e8efff 0%, #f3f6fb 42%);
|
|
--panel: #ffffff;
|
|
--text: #172435;
|
|
--muted: #5c6a7c;
|
|
--line: #d7e0ee;
|
|
--line-strong: #a9bad7;
|
|
--blue: #2059d8;
|
|
--teal: #0d9f9e;
|
|
--green: #0fa57d;
|
|
--amber: #cf8a11;
|
|
--red: #d63f50;
|
|
--chip: #f7faff;
|
|
--chip-border: #d4deef;
|
|
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
--sans: "Segoe UI", "Avenir Next", "Helvetica Neue", Arial, sans-serif;
|
|
--shadow: 0 8px 24px rgba(14, 25, 42, 0.08);
|
|
--radius: 14px;
|
|
}
|
|
|
|
html[data-theme="dark"] {
|
|
--bg: #0d1420;
|
|
--bg-grad: radial-gradient(circle at 0% 0%, #1a2538 0%, #0d1420 44%);
|
|
--panel: #131c2a;
|
|
--text: #e6edf9;
|
|
--muted: #9eb0cb;
|
|
--line: #26344a;
|
|
--line-strong: #355079;
|
|
--blue: #4b86ff;
|
|
--teal: #25c9c4;
|
|
--green: #24c794;
|
|
--amber: #e9ad42;
|
|
--red: #ff6f7b;
|
|
--chip: #172234;
|
|
--chip-border: #324968;
|
|
--shadow: 0 8px 24px rgba(0, 0, 0, 0.34);
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
background: var(--bg-grad), var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--sans);
|
|
line-height: 1.42;
|
|
}
|
|
|
|
.wrap { max-width: 1240px; margin: 0 auto; padding: 18px; }
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.btn {
|
|
border: 1px solid var(--line-strong);
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
padding: 7px 11px;
|
|
border-radius: 9px;
|
|
font-size: 0.82rem;
|
|
cursor: pointer;
|
|
transition: background 0.15s ease, border-color 0.15s ease;
|
|
}
|
|
|
|
.btn:hover { border-color: var(--blue); }
|
|
.btn.active { background: var(--blue); color: #fff; border-color: var(--blue); }
|
|
|
|
.hero {
|
|
background: linear-gradient(128deg, #1e47ac 0%, #1f5ad6 48%, #1a8eb9 100%);
|
|
color: #fff;
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
padding: 20px;
|
|
display: grid;
|
|
grid-template-columns: 1.4fr 1fr;
|
|
gap: 14px;
|
|
}
|
|
|
|
.hero h1 { margin: 0 0 7px; font-size: 1.58rem; letter-spacing: 0.2px; }
|
|
.hero p { margin: 0; opacity: 0.94; font-size: 0.93rem; }
|
|
|
|
.badge-row { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 7px; }
|
|
.badge {
|
|
padding: 4px 9px;
|
|
border: 1px solid rgba(255,255,255,0.3);
|
|
background: rgba(255,255,255,0.12);
|
|
border-radius: 999px;
|
|
font-size: 0.77rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.metric-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0,1fr));
|
|
gap: 8px;
|
|
align-content: start;
|
|
}
|
|
|
|
.metric {
|
|
border: 1px solid rgba(255,255,255,0.28);
|
|
background: rgba(255,255,255,0.13);
|
|
border-radius: 10px;
|
|
padding: 8px;
|
|
}
|
|
|
|
.metric .k { font-size: 0.7rem; text-transform: uppercase; opacity: 0.86; letter-spacing: 0.45px; }
|
|
.metric .v { font-size: 1rem; font-weight: 700; margin-top: 2px; }
|
|
|
|
.grid { margin-top: 14px; display: grid; gap: 12px; }
|
|
|
|
.panel {
|
|
background: var(--panel);
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
box-shadow: var(--shadow);
|
|
padding: 14px;
|
|
}
|
|
|
|
.panel h2 { margin: 0 0 10px; font-size: 1.03rem; }
|
|
.muted { color: var(--muted); font-size: 0.84rem; }
|
|
|
|
.split { display: grid; grid-template-columns: 1.2fr 1fr; gap: 12px; }
|
|
|
|
.chip-list { display: flex; flex-wrap: wrap; gap: 7px; }
|
|
.chip {
|
|
border: 1px solid var(--chip-border);
|
|
background: var(--chip);
|
|
color: var(--text);
|
|
border-radius: 999px;
|
|
font-size: 0.77rem;
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.kpi-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0,1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.kpi {
|
|
border: 1px solid var(--line);
|
|
border-radius: 10px;
|
|
background: var(--panel);
|
|
padding: 10px;
|
|
}
|
|
|
|
.kpi .label { color: var(--muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.38px; }
|
|
.kpi .value { font-size: 1.15rem; font-weight: 700; margin-top: 4px; }
|
|
|
|
.table-tools { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
|
|
table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
|
|
th, td {
|
|
text-align: left;
|
|
vertical-align: top;
|
|
border-bottom: 1px solid var(--line);
|
|
border-right: 1px solid color-mix(in srgb, var(--line) 82%, transparent);
|
|
padding: 7px 6px;
|
|
}
|
|
th:last-child, td:last-child { border-right: 0; }
|
|
th {
|
|
color: var(--muted);
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.35px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
tr.group-row td {
|
|
background: color-mix(in srgb, var(--panel) 80%, var(--line));
|
|
color: var(--text);
|
|
font-weight: 700;
|
|
border-bottom: 1px solid var(--line-strong);
|
|
border-top: 1px solid var(--line-strong);
|
|
text-transform: uppercase;
|
|
font-size: 0.72rem;
|
|
letter-spacing: 0.45px;
|
|
}
|
|
|
|
code {
|
|
font-family: var(--mono);
|
|
background: color-mix(in srgb, var(--blue) 10%, transparent);
|
|
color: var(--text);
|
|
border-radius: 6px;
|
|
padding: 1px 5px;
|
|
font-size: 0.76rem;
|
|
}
|
|
|
|
.svg-box {
|
|
border: 1px solid var(--line);
|
|
border-radius: 10px;
|
|
padding: 10px;
|
|
background: color-mix(in srgb, var(--panel) 92%, var(--line));
|
|
}
|
|
|
|
.graph-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
|
|
.timeline { display: grid; gap: 8px; }
|
|
.step { display: grid; grid-template-columns: 28px 1fr; gap: 8px; align-items: start; }
|
|
.dot {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 999px;
|
|
color: #fff;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
}
|
|
.d1 { background: var(--blue); }
|
|
.d2 { background: var(--teal); }
|
|
.d3 { background: var(--amber); }
|
|
.d4 { background: var(--green); }
|
|
|
|
details {
|
|
border: 1px solid var(--line);
|
|
border-radius: 10px;
|
|
padding: 8px 10px;
|
|
background: color-mix(in srgb, var(--panel) 96%, var(--line));
|
|
}
|
|
|
|
details + details { margin-top: 8px; }
|
|
summary {
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
user-select: none;
|
|
}
|
|
|
|
.risk {
|
|
border-left: 4px solid var(--amber);
|
|
background: color-mix(in srgb, var(--amber) 10%, transparent);
|
|
border-radius: 8px;
|
|
padding: 8px 10px;
|
|
margin-top: 7px;
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.risk.ok { border-left-color: var(--green); background: color-mix(in srgb, var(--green) 10%, transparent); }
|
|
.risk.bad { border-left-color: var(--red); background: color-mix(in srgb, var(--red) 10%, transparent); }
|
|
|
|
.foot {
|
|
font-size: 0.76rem;
|
|
color: var(--muted);
|
|
margin-top: 6px;
|
|
}
|
|
|
|
@media (max-width: 1040px) {
|
|
.hero, .split, .graph-grid { grid-template-columns: 1fr; }
|
|
.kpi-cards { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.kpi-cards { grid-template-columns: 1fr; }
|
|
.table-tools { flex-wrap: wrap; }
|
|
table { font-size: 0.78rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="wrap">
|
|
<div class="toolbar">
|
|
<button id="themeToggle" class="btn" type="button">Toggle Dark Mode</button>
|
|
</div>
|
|
|
|
<section class="hero">
|
|
<div>
|
|
<h1>RotatingMachine Engineering Anchor</h1>
|
|
<p>Function-design truth source for runtime behavior, control contracts, units/signals, and integration boundaries.</p>
|
|
<div class="badge-row">
|
|
<span class="badge">Node Type: <code>rotatingMachine</code></span>
|
|
<span class="badge">Domain Class: <code>specificClass.js</code></span>
|
|
<span class="badge">Wrapper: <code>nodeClass.js</code></span>
|
|
<span class="badge">Ports: <code>3</code></span>
|
|
</div>
|
|
</div>
|
|
<div class="metric-grid">
|
|
<div class="metric"><div class="k">Control Topics In</div><div class="v">9</div></div>
|
|
<div class="metric"><div class="k">Signal Rows Catalogued</div><div class="v">31</div></div>
|
|
<div class="metric"><div class="k">Anchored Tests</div><div class="v">9</div></div>
|
|
<div class="metric"><div class="k">Known Risks</div><div class="v">4</div></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="grid">
|
|
<section class="panel">
|
|
<h2>Connection Map (Always Visible)</h2>
|
|
<div class="split">
|
|
<div>
|
|
<div class="muted">Primary integrations and contracts</div>
|
|
<div class="chip-list" style="margin-top:8px; margin-bottom:10px">
|
|
<span class="chip">machineGroupControl -> parent command source</span>
|
|
<span class="chip">pumpingStation -> orchestration source</span>
|
|
<span class="chip">measurement -> real pressure child via registerChild</span>
|
|
<span class="chip">dashboard flows -> simulateMeasurement + chart consumers</span>
|
|
<span class="chip">output[2] -> registerChild to parent</span>
|
|
<span class="chip">/rotatingMachine/menu.js</span>
|
|
<span class="chip">/rotatingMachine/configData.js</span>
|
|
</div>
|
|
<div class="kpi-cards">
|
|
<div class="kpi"><div class="label">Pressure Policy</div><div class="value">Real > Virtual > 0</div></div>
|
|
<div class="kpi"><div class="label">Mode Gate</div><div class="value">Action + Source</div></div>
|
|
<div class="kpi"><div class="label">Tick Rate</div><div class="value">1s</div></div>
|
|
<div class="kpi"><div class="label">Operational States</div><div class="value">4 Active</div></div>
|
|
</div>
|
|
</div>
|
|
<div class="svg-box">
|
|
<svg viewBox="0 0 540 230" width="100%" role="img" aria-label="integration topology">
|
|
<defs>
|
|
<marker id="arr" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
|
<path d="M0,0 L0,6 L9,3 z" fill="#6f85aa"></path>
|
|
</marker>
|
|
</defs>
|
|
<rect x="200" y="86" width="140" height="52" rx="10" fill="#2059d8"/>
|
|
<text x="270" y="117" text-anchor="middle" fill="#fff" font-size="13" font-family="Segoe UI">rotatingMachine</text>
|
|
|
|
<rect x="18" y="20" width="160" height="44" rx="9" fill="#e9f1ff" stroke="#aac0ef"/>
|
|
<text x="98" y="46" text-anchor="middle" fill="#244271" font-size="12">machineGroupControl</text>
|
|
|
|
<rect x="18" y="164" width="160" height="44" rx="9" fill="#e7faf5" stroke="#9edcca"/>
|
|
<text x="98" y="190" text-anchor="middle" fill="#1e5244" font-size="12">pumpingStation</text>
|
|
|
|
<rect x="360" y="24" width="160" height="44" rx="9" fill="#fff5e5" stroke="#e9c589"/>
|
|
<text x="440" y="50" text-anchor="middle" fill="#634319" font-size="12">dashboard / examples</text>
|
|
|
|
<rect x="360" y="156" width="160" height="44" rx="9" fill="#eef8e8" stroke="#b7d89e"/>
|
|
<text x="440" y="182" text-anchor="middle" fill="#2e5a22" font-size="12">measurement</text>
|
|
|
|
<line x1="178" y1="44" x2="200" y2="96" stroke="#6f85aa" stroke-width="2" marker-end="url(#arr)"/>
|
|
<line x1="178" y1="186" x2="200" y2="130" stroke="#6f85aa" stroke-width="2" marker-end="url(#arr)"/>
|
|
<line x1="360" y1="45" x2="338" y2="97" stroke="#6f85aa" stroke-width="2" marker-end="url(#arr)"/>
|
|
<line x1="341" y1="130" x2="360" y2="50" stroke="#8ea4c8" stroke-dasharray="4 4" stroke-width="2" marker-end="url(#arr)"/>
|
|
<line x1="360" y1="178" x2="340" y2="126" stroke="#6f85aa" stroke-width="2" marker-end="url(#arr)"/>
|
|
|
|
<text x="184" y="66" fill="#5c7091" font-size="11">setMode / exec*</text>
|
|
<text x="182" y="162" fill="#5c7091" font-size="11">station-level dispatch</text>
|
|
<text x="334" y="78" fill="#5c7091" font-size="11">simulateMeasurement</text>
|
|
<text x="350" y="94" fill="#5c7091" font-size="11">process + influx out</text>
|
|
<text x="342" y="168" fill="#5c7091" font-size="11">registerChild + pressure.measured.*</text>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Engineering Unit & Signal Catalog</h2>
|
|
<div class="table-tools">
|
|
<button id="btnInput" class="btn active" type="button">Input</button>
|
|
<button id="btnOutput" class="btn" type="button">Output</button>
|
|
<span class="muted" id="tableMeta">Showing input topics/signals grouped by engineering function.</span>
|
|
</div>
|
|
<table id="signalTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Group</th>
|
|
<th>Signal / Topic</th>
|
|
<th>Direction</th>
|
|
<th>Unit</th>
|
|
<th>Typical Range</th>
|
|
<th>Criticality</th>
|
|
<th>Meaning</th>
|
|
<th>Primary Source</th>
|
|
<th>Conversion Hints</th>
|
|
<th>Fallback / Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Functional Parameter Sheet (Template + Example)</h2>
|
|
<div class="muted">Engineering-oriented parameter table for BEP/curve/mechanical context. Example values are placeholders and scenario-dependent.</div>
|
|
<table id="paramTable" style="margin-top:10px">
|
|
<thead>
|
|
<tr>
|
|
<th>Parameter</th>
|
|
<th>Symbool</th>
|
|
<th>Eenheid</th>
|
|
<th>Waarde (+/-)</th>
|
|
<th>Toelichting</th>
|
|
<th>Node Mapping</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="paramBody"></tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>True Graphs (Data-Derived)</h2>
|
|
<div class="muted">Curves below are drawn from repository data in <code>nodes/rotatingMachine/misc/measured_curve.json</code> (pressure slice <code>175</code>), plus derived efficiency ratio.</div>
|
|
<div class="graph-grid" style="margin-top:10px">
|
|
<div class="svg-box">
|
|
<div class="muted" style="margin-bottom:6px">Flow vs Control (pressure=175)</div>
|
|
<svg id="flowChart" viewBox="0 0 520 280" width="100%" role="img" aria-label="flow vs control"></svg>
|
|
</div>
|
|
<div class="svg-box">
|
|
<div class="muted" style="margin-bottom:6px">Power vs Control (pressure=175)</div>
|
|
<svg id="powerChart" viewBox="0 0 520 280" width="100%" role="img" aria-label="power vs control"></svg>
|
|
</div>
|
|
</div>
|
|
<div class="graph-grid" style="margin-top:10px">
|
|
<div class="svg-box">
|
|
<div class="muted" style="margin-bottom:6px">Derived Efficiency Index (flow/power) vs Control</div>
|
|
<svg id="effChart" viewBox="0 0 520 280" width="100%" role="img" aria-label="efficiency index vs control"></svg>
|
|
</div>
|
|
<div class="svg-box">
|
|
<div class="muted" style="margin-bottom:6px">Allowed Actions by Mode (config defaults)</div>
|
|
<svg id="modeChart" viewBox="0 0 520 280" width="100%" role="img" aria-label="allowed actions by mode"></svg>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Execution Flow (Core Open)</h2>
|
|
<div class="timeline">
|
|
<div class="step"><div class="dot d1">1</div><div><strong>Construct</strong><div class="muted">Load defaults and model, initialize predictors/state/measurement containers, attach state listeners.</div></div></div>
|
|
<div class="step"><div class="dot d2">2</div><div><strong>Connect</strong><div class="muted">Register virtual pressure children, listen to real/virtual measurement streams, register self to parent.</div></div></div>
|
|
<div class="step"><div class="dot d3">3</div><div><strong>Control</strong><div class="muted">Route topics, validate mode/source/action, execute movement or sequence transitions.</div></div></div>
|
|
<div class="step"><div class="dot d4">4</div><div><strong>Compute + Emit</strong><div class="muted">Pressure basis selection -> flow/power prediction -> efficiency/CoG/BEP -> output formatting and status update.</div></div></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Extended Sections (Selective Collapse)</h2>
|
|
<details>
|
|
<summary>Function Inventory Snapshot</summary>
|
|
<div class="muted" style="margin-top:8px">Anchored files: <code>rotatingMachine.js</code>, <code>src/nodeClass.js</code>, <code>src/specificClass.js</code>. 44+ callable methods/paths inventoried in markdown anchor.</div>
|
|
<div class="chip-list" style="margin-top:8px">
|
|
<span class="chip">constructor + init</span>
|
|
<span class="chip">registerChild + handler dispatch</span>
|
|
<span class="chip">handleInput + setMode + sequences</span>
|
|
<span class="chip">calcFlow/calcPower/calcCtrl</span>
|
|
<span class="chip">calcEfficiency + calcCog + getOutput</span>
|
|
</div>
|
|
</details>
|
|
|
|
<details>
|
|
<summary>Risks And Invariants</summary>
|
|
<div class="risk bad"><strong>Risk:</strong> <code>CoG</code> input path calls <code>showCoG()</code> but method is not present in current <code>specificClass.js</code>.</div>
|
|
<div class="risk bad"><strong>Risk:</strong> emergency sequence key mismatch (<code>emergencyStop</code> vs config <code>emergencystop</code>).</div>
|
|
<div class="risk"><strong>Risk:</strong> <code>eneableLog</code> typo in state logging mapping.</div>
|
|
<div class="risk"><strong>Risk:</strong> node label expression precedence may render unexpected labels.</div>
|
|
<div class="risk ok"><strong>Invariant:</strong> mechanical truth remains in <code>specificClass.js</code>; wrapper remains routing/lifecycle.</div>
|
|
<div class="risk ok"><strong>Invariant:</strong> pressure selection order stays real sensor > virtual sensor > fallback 0.</div>
|
|
<div class="risk ok"><strong>Invariant:</strong> output channels remain process/influx/parent registration separation.</div>
|
|
</details>
|
|
|
|
<details>
|
|
<summary>Test Evidence</summary>
|
|
<div class="chip-list" style="margin-top:8px">
|
|
<span class="chip">constructor.basic.test.js</span>
|
|
<span class="chip">mode-and-input.basic.test.js</span>
|
|
<span class="chip">error-paths.edge.test.js</span>
|
|
<span class="chip">nodeClass-routing.edge.test.js</span>
|
|
<span class="chip">sequences.integration.test.js</span>
|
|
<span class="chip">registration.integration.test.js</span>
|
|
<span class="chip">pressure-initialization.integration.test.js</span>
|
|
<span class="chip">coolprop.integration.test.js</span>
|
|
<span class="chip">basic-flow-dashboard.integration.test.js</span>
|
|
</div>
|
|
</details>
|
|
</section>
|
|
|
|
<div class="foot">
|
|
Iteration 3: engineering-grade signal table (ranges, criticality, conversion hints), functional parameter sheet, input/output filters, dark mode toggle, selective collapsible sections, and data-derived graphs.
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
(function () {
|
|
const signalRows = [
|
|
{ dir: "input", group: "Control Topic", signal: "topic:registerChild", unit: "n/a", meaning: "Attach child node source by node id", source: "nodeClass.js:268", notes: "Warns if child/source not found" },
|
|
{ dir: "input", group: "Control Topic", signal: "topic:setMode", unit: "enum", meaning: "Set current command policy mode", source: "nodeClass.js:278", notes: "Validated by setMode()" },
|
|
{ dir: "input", group: "Control Topic", signal: "topic:execSequence", unit: "json payload", meaning: "Execute named transition sequence", source: "nodeClass.js:281", notes: "Mode + source gated" },
|
|
{ dir: "input", group: "Control Topic", signal: "topic:execMovement", unit: "json payload", meaning: "Move to explicit setpoint", source: "nodeClass.js:285", notes: "Setpoint coerced to Number" },
|
|
{ dir: "input", group: "Control Topic", signal: "topic:flowMovement", unit: "json payload", meaning: "Convert desired flow to control setpoint", source: "nodeClass.js:289", notes: "Uses calcCtrl()" },
|
|
{ dir: "input", group: "Control Topic", signal: "topic:emergencystop", unit: "json payload", meaning: "Emergency stop command path", source: "nodeClass.js:294", notes: "Sequence-name mismatch risk in logic" },
|
|
{ dir: "input", group: "Control Topic", signal: "topic:simulateMeasurement", unit: "json payload", meaning: "Inject measured values from dashboard/test flow", source: "nodeClass.js:298", notes: "Finite numeric value required" },
|
|
{ dir: "input", group: "Control Topic", signal: "topic:showWorkingCurves", unit: "n/a", meaning: "Request current curve and CoG diagnostics", source: "nodeClass.js:336", notes: "Immediate response on output[0]" },
|
|
{ dir: "input", group: "Control Topic", signal: "topic:CoG", unit: "n/a", meaning: "Request CoG diagnostics", source: "nodeClass.js:339", notes: "Calls showCoG() method" },
|
|
|
|
{ dir: "input", group: "Measured Signal", signal: "pressure.measured.upstream", unit: "mbar (default)", meaning: "Upstream pressure input", source: "specificClass.js:52,703", notes: "Real child preferred over virtual" },
|
|
{ dir: "input", group: "Measured Signal", signal: "pressure.measured.downstream", unit: "mbar (default)", meaning: "Downstream pressure input", source: "specificClass.js:52,703", notes: "Combined with upstream for differential" },
|
|
{ dir: "input", group: "Measured Signal", signal: "flow.measured.upstream", unit: "config general.unit", meaning: "Measured inflow reference", source: "specificClass.js:727", notes: "Used for reconciliation when present" },
|
|
{ dir: "input", group: "Measured Signal", signal: "flow.measured.downstream", unit: "config general.unit", meaning: "Measured discharge reference", source: "specificClass.js:727", notes: "Accepted only in operational states" },
|
|
{ dir: "input", group: "Measured Signal", signal: "temperature.measured.atEquipment", unit: "C (init), K used", meaning: "Fluid temperature for density/efficiency path", source: "specificClass.js:149,858", notes: "Starts at 15 C" },
|
|
{ dir: "input", group: "Measured Signal", signal: "power.measured.atEquipment", unit: "kW", meaning: "Measured power if provided by child", source: "specificClass.js:686", notes: "Read helper exists" },
|
|
|
|
{ dir: "output", group: "Port Contract", signal: "output[0]", unit: "process msg", meaning: "Process payload from flattened output", source: "nodeClass.js:250", notes: "Formatted by outputUtils" },
|
|
{ dir: "output", group: "Port Contract", signal: "output[1]", unit: "influx msg", meaning: "InfluxDB payload", source: "nodeClass.js:251", notes: "Formatted by outputUtils" },
|
|
{ dir: "output", group: "Port Contract", signal: "output[2]", unit: "registerChild msg", meaning: "Parent registration plumbing", source: "nodeClass.js:222", notes: "Topic registerChild" },
|
|
|
|
{ dir: "output", group: "Predicted Signal", signal: "flow.predicted.downstream", unit: "config general.unit", meaning: "Predicted discharge flow", source: "specificClass.js:423", notes: "0 when inactive/no curve" },
|
|
{ dir: "output", group: "Predicted Signal", signal: "flow.predicted.atEquipment", unit: "config general.unit", meaning: "Predicted flow at equipment", source: "specificClass.js:424", notes: "0 when inactive/no curve" },
|
|
{ dir: "output", group: "Predicted Signal", signal: "flow.predicted.min", unit: "config general.unit", meaning: "Curve min flow at current pressure", source: "specificClass.js:156", notes: "Seeded in init/fallback paths" },
|
|
{ dir: "output", group: "Predicted Signal", signal: "flow.predicted.max", unit: "config general.unit", meaning: "Curve max flow at current pressure", source: "specificClass.js:155", notes: "Seeded in init/fallback paths" },
|
|
{ dir: "output", group: "Predicted Signal", signal: "power.predicted.atEquipment", unit: "kW", meaning: "Predicted power draw", source: "specificClass.js:448", notes: "0 when inactive/no curve" },
|
|
{ dir: "output", group: "Predicted Signal", signal: "ctrl.predicted.atEquipment", unit: "unitless (%)", meaning: "Predicted control setpoint", source: "specificClass.js:482", notes: "From requested flow" },
|
|
|
|
{ dir: "output", group: "Derived KPI", signal: "efficiency.predicted.atEquipment", unit: "flow/power ratio", meaning: "Specific flow proxy", source: "specificClass.js:881", notes: "Computed when power and flow non-zero" },
|
|
{ dir: "output", group: "Derived KPI", signal: "specificEnergyConsumption.predicted.atEquipment", unit: "power/flow", meaning: "Specific energy proxy", source: "specificClass.js:882", notes: "Computed when power and flow non-zero" },
|
|
{ dir: "output", group: "Derived KPI", signal: "nHydraulicEfficiency.predicted.atEquipment", unit: "unitless", meaning: "Hydraulic-efficiency-like metric", source: "specificClass.js:887", notes: "Uses pressure diff + density + conversions" },
|
|
{ dir: "output", group: "Derived KPI", signal: "cog / NCog / NCogPercent", unit: "unitless / %", meaning: "Best efficiency operating index", source: "specificClass.js:796,950", notes: "Used by higher-level optimization" },
|
|
{ dir: "output", group: "Derived KPI", signal: "effDistFromPeak / effRelDistFromPeak", unit: "unitless", meaning: "Distance from best-efficiency point", source: "specificClass.js:924,962", notes: "Absolute + relative" },
|
|
|
|
{ dir: "output", group: "Runtime State", signal: "state", unit: "enum", meaning: "Current movement/state-machine state", source: "specificClass.js:943", notes: "Exposed in output object" },
|
|
{ dir: "output", group: "Runtime State", signal: "mode", unit: "enum", meaning: "Current command mode", source: "specificClass.js:947", notes: "auto/virtualControl/fysicalControl" },
|
|
{ dir: "output", group: "Runtime State", signal: "ctrl", unit: "%", meaning: "Current position setpoint", source: "specificClass.js:945", notes: "From state module" },
|
|
{ dir: "output", group: "Runtime State", signal: "runtime", unit: "h", meaning: "Accumulated runtime", source: "specificClass.js:944", notes: "From state module" },
|
|
{ dir: "output", group: "Runtime State", signal: "moveTimeleft", unit: "s", meaning: "Time remaining for movement", source: "specificClass.js:946", notes: "From state module" },
|
|
{ dir: "output", group: "Runtime State", signal: "maintenanceTime", unit: "h", meaning: "Maintenance counter", source: "specificClass.js:951", notes: "From state module" },
|
|
{ dir: "output", group: "Runtime State", signal: "flowNrmse / flowLongterNRMSD / flowImmediateLevel / flowLongTermLevel", unit: "mixed", meaning: "Drift diagnostics when available", source: "specificClass.js:953", notes: "Present only when flowDrift exists" }
|
|
];
|
|
|
|
const tbody = document.querySelector("#signalTable tbody");
|
|
const btnInput = document.getElementById("btnInput");
|
|
const btnOutput = document.getElementById("btnOutput");
|
|
const tableMeta = document.getElementById("tableMeta");
|
|
const paramBody = document.getElementById("paramBody");
|
|
let activeDir = "input";
|
|
|
|
function inferTypicalRange(r) {
|
|
if (r.signal.indexOf("pressure.measured.upstream") >= 0) return "200-1500 mbar";
|
|
if (r.signal.indexOf("pressure.measured.downstream") >= 0) return "400-2500 mbar";
|
|
if (r.signal.indexOf("flow.measured") >= 0) return "0-2.5 m3/s or project unit";
|
|
if (r.signal.indexOf("flow.predicted") >= 0) return "Curve-bound min-max";
|
|
if (r.signal.indexOf("power.measured") >= 0) return "0-250 kW";
|
|
if (r.signal.indexOf("power.predicted") >= 0) return "Curve-bound min-max";
|
|
if (r.signal.indexOf("temperature.measured") >= 0) return "5-40 C (278-313 K)";
|
|
if (r.signal.indexOf("ctrl") >= 0) return "0-100% (project may scale)";
|
|
if (r.signal.indexOf("runtime") >= 0 || r.signal.indexOf("maintenanceTime") >= 0) return "0+ h";
|
|
if (r.signal.indexOf("moveTimeleft") >= 0) return "0+ s";
|
|
if (r.signal.indexOf("efficiency") >= 0 || r.signal.indexOf("nHydraulicEfficiency") >= 0) return "0-1+ (index)";
|
|
if (r.signal.indexOf("NCog") >= 0 || r.signal.indexOf("cog") >= 0) return "0-1 (NCog), 0-100% (NCogPercent)";
|
|
if (r.group === "Control Topic") return "Discrete commands";
|
|
if (r.group === "Port Contract") return "Per tick / event message";
|
|
return "Project-specific";
|
|
}
|
|
|
|
function inferCriticality(r) {
|
|
if (r.signal.indexOf("emergencystop") >= 0) return "High";
|
|
if (r.signal.indexOf("setMode") >= 0 || r.signal.indexOf("execSequence") >= 0 || r.signal.indexOf("execMovement") >= 0 || r.signal.indexOf("flowMovement") >= 0) return "High";
|
|
if (r.signal.indexOf("pressure.measured") >= 0) return "High";
|
|
if (r.signal.indexOf("power.predicted") >= 0 || r.signal.indexOf("flow.predicted") >= 0 || r.signal.indexOf("output[") >= 0) return "High";
|
|
if (r.signal.indexOf("temperature.measured") >= 0 || r.signal.indexOf("power.measured") >= 0) return "Medium";
|
|
if (r.signal.indexOf("efficiency") >= 0 || r.signal.indexOf("NCog") >= 0 || r.signal.indexOf("flowNrmse") >= 0) return "Medium";
|
|
if (r.signal.indexOf("showWorkingCurves") >= 0 || r.signal.indexOf("CoG") >= 0) return "Low";
|
|
return "Medium";
|
|
}
|
|
|
|
function inferConversionHints(r) {
|
|
if (r.signal.indexOf("pressure") >= 0) return "mbar <-> Pa (1 mbar = 100 Pa)";
|
|
if (r.signal.indexOf("flow") >= 0) return "m3/h <-> m3/s (divide/multiply by 3600); l/s <-> m3/s (x/1000)";
|
|
if (r.signal.indexOf("power") >= 0) return "kW <-> W (x1000)";
|
|
if (r.signal.indexOf("temperature") >= 0) return "C <-> K (+/-273.15)";
|
|
if (r.signal.indexOf("ctrl") >= 0) return "Unitless ratio <-> % (x100)";
|
|
if (r.signal.indexOf("runtime") >= 0 || r.signal.indexOf("maintenanceTime") >= 0) return "h <-> s (x3600)";
|
|
return "No conversion required / message contract";
|
|
}
|
|
|
|
function renderParameterSheet() {
|
|
const params = [
|
|
{ p: "Nominaal toerental", s: "n", u: "t/min", v: "1477 (voorbeeld)", t: "50 Hz in bedrijf", m: "Not directly output; machine/asset datasheet parameter" },
|
|
{ p: "Debiet bij BEP", s: "Q", u: "L/s", v: "86 (voorbeeld)", t: "Beste efficientiepunt", m: "Derivable from curve + CoG/efficiency path" },
|
|
{ p: "Opvoerhoogte", s: "H", u: "m", v: "11 (voorbeeld)", t: "Bij Q = 86 L/s", m: "Related to pressure differential conversion path" },
|
|
{ p: "Opgenomen vermogen", s: "P2", u: "kW", v: "13-15 (voorbeeld)", t: "Inclusief mechanische verliezen", m: "output: power.predicted.atEquipment / measured power path" },
|
|
{ p: "Rendement", s: "η", u: "%", v: "73 (voorbeeld)", t: "Maximaal rendement", m: "output: efficiency.* and nHydraulicEfficiency.*" },
|
|
{ p: "Benodigde NPSH", s: "NPSHr", u: "m", v: "2-3 (voorbeeld)", t: "Lage cavitatie gevoeligheid", m: "Not explicit in current output; candidate future anchor metric" },
|
|
{ p: "Persaansluiting", s: "-", u: "DN150 / PN16", v: "-", t: "Horizontale uitlaat", m: "Asset/mechanical metadata (datasheet-level)" },
|
|
{ p: "Zuigaansluiting", s: "-", u: "DN200 / PN10", v: "-", t: "Instroomzijde pomp", m: "Asset/mechanical metadata (datasheet-level)" },
|
|
{ p: "Pomphuismateriaal", s: "-", u: "EN-GJL-250", v: "-", t: "Gietijzer", m: "Asset/mechanical metadata (datasheet-level)" },
|
|
{ p: "Waaiermateriaal", s: "-", u: "1.4122 RVS", v: "-", t: "Schroefcentrifugaalwaaier", m: "Asset/mechanical metadata (datasheet-level)" },
|
|
{ p: "Pompgewicht", s: "-", u: "kg", v: "183 (voorbeeld)", t: "Droge uitvoering", m: "Asset/mechanical metadata (datasheet-level)" }
|
|
];
|
|
|
|
paramBody.innerHTML = params.map(function (row) {
|
|
return "<tr>" +
|
|
"<td>" + row.p + "</td>" +
|
|
"<td><code>" + row.s + "</code></td>" +
|
|
"<td>" + row.u + "</td>" +
|
|
"<td>" + row.v + "</td>" +
|
|
"<td>" + row.t + "</td>" +
|
|
"<td>" + row.m + "</td>" +
|
|
"</tr>";
|
|
}).join("");
|
|
}
|
|
|
|
function renderRows() {
|
|
const rows = signalRows.filter((r) => r.dir === activeDir);
|
|
const groups = [...new Set(rows.map((r) => r.group))];
|
|
tbody.innerHTML = "";
|
|
|
|
groups.forEach((group) => {
|
|
const gtr = document.createElement("tr");
|
|
gtr.className = "group-row";
|
|
const gtd = document.createElement("td");
|
|
gtd.colSpan = 10;
|
|
gtd.textContent = group;
|
|
gtr.appendChild(gtd);
|
|
tbody.appendChild(gtr);
|
|
|
|
rows.filter((r) => r.group === group).forEach((r) => {
|
|
const typicalRange = inferTypicalRange(r);
|
|
const criticality = inferCriticality(r);
|
|
const conversionHints = inferConversionHints(r);
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML = "<td>" + r.group + "</td>" +
|
|
"<td><code>" + r.signal + "</code></td>" +
|
|
"<td>" + r.dir + "</td>" +
|
|
"<td>" + r.unit + "</td>" +
|
|
"<td>" + typicalRange + "</td>" +
|
|
"<td>" + criticality + "</td>" +
|
|
"<td>" + r.meaning + "</td>" +
|
|
"<td><code>" + r.source + "</code></td>" +
|
|
"<td>" + conversionHints + "</td>" +
|
|
"<td>" + r.notes + "</td>";
|
|
tbody.appendChild(tr);
|
|
});
|
|
});
|
|
|
|
tableMeta.textContent = activeDir === "input"
|
|
? "Showing input topics/signals grouped by engineering function."
|
|
: "Showing output ports/signals/KPIs grouped by engineering function.";
|
|
}
|
|
|
|
btnInput.addEventListener("click", function () {
|
|
activeDir = "input";
|
|
btnInput.classList.add("active");
|
|
btnOutput.classList.remove("active");
|
|
renderRows();
|
|
});
|
|
|
|
btnOutput.addEventListener("click", function () {
|
|
activeDir = "output";
|
|
btnOutput.classList.add("active");
|
|
btnInput.classList.remove("active");
|
|
renderRows();
|
|
});
|
|
|
|
const themeToggle = document.getElementById("themeToggle");
|
|
const root = document.documentElement;
|
|
const storedTheme = localStorage.getItem("rm_anchor_theme");
|
|
if (storedTheme === "dark") {
|
|
root.setAttribute("data-theme", "dark");
|
|
}
|
|
|
|
themeToggle.addEventListener("click", function () {
|
|
const isDark = root.getAttribute("data-theme") === "dark";
|
|
const next = isDark ? "light" : "dark";
|
|
root.setAttribute("data-theme", next);
|
|
localStorage.setItem("rm_anchor_theme", next);
|
|
drawAllCharts();
|
|
});
|
|
|
|
function drawLineChart(svgId, options) {
|
|
const svg = document.getElementById(svgId);
|
|
const w = 520;
|
|
const h = 280;
|
|
const m = { top: 20, right: 16, bottom: 44, left: 56 };
|
|
const iw = w - m.left - m.right;
|
|
const ih = h - m.top - m.bottom;
|
|
|
|
const xMin = Math.min.apply(null, options.x);
|
|
const xMax = Math.max.apply(null, options.x);
|
|
const yMinRaw = Math.min.apply(null, options.y);
|
|
const yMaxRaw = Math.max.apply(null, options.y);
|
|
const yPad = (yMaxRaw - yMinRaw) * 0.12 || 1;
|
|
const yMin = Math.max(0, yMinRaw - yPad);
|
|
const yMax = yMaxRaw + yPad;
|
|
|
|
function sx(v) { return m.left + ((v - xMin) / (xMax - xMin || 1)) * iw; }
|
|
function sy(v) { return m.top + ih - ((v - yMin) / (yMax - yMin || 1)) * ih; }
|
|
|
|
const textColor = getComputedStyle(document.documentElement).getPropertyValue("--muted").trim() || "#627489";
|
|
const lineColor = options.color;
|
|
const axisColor = getComputedStyle(document.documentElement).getPropertyValue("--line-strong").trim() || "#96accb";
|
|
const gridColor = getComputedStyle(document.documentElement).getPropertyValue("--line").trim() || "#d7e0ee";
|
|
|
|
let path = "";
|
|
options.x.forEach(function (xv, i) {
|
|
const px = sx(xv);
|
|
const py = sy(options.y[i]);
|
|
path += (i === 0 ? "M" : " L") + px + " " + py;
|
|
});
|
|
|
|
const yTicks = 5;
|
|
let tickEls = "";
|
|
for (let i = 0; i <= yTicks; i++) {
|
|
const val = yMin + ((yMax - yMin) * i) / yTicks;
|
|
const py = sy(val);
|
|
tickEls += '<line x1="' + m.left + '" y1="' + py + '" x2="' + (w - m.right) + '" y2="' + py + '" stroke="' + gridColor + '" stroke-width="1" />';
|
|
tickEls += '<text x="' + (m.left - 8) + '" y="' + (py + 4) + '" text-anchor="end" fill="' + textColor + '" font-size="11">' + val.toFixed(2) + '</text>';
|
|
}
|
|
|
|
let pointEls = "";
|
|
options.x.forEach(function (xv, i) {
|
|
pointEls += '<circle cx="' + sx(xv) + '" cy="' + sy(options.y[i]) + '" r="3.8" fill="' + lineColor + '" />';
|
|
});
|
|
|
|
let xTickEls = "";
|
|
options.x.forEach(function (xv) {
|
|
xTickEls += '<line x1="' + sx(xv) + '" y1="' + (h - m.bottom) + '" x2="' + sx(xv) + '" y2="' + (h - m.bottom + 5) + '" stroke="' + axisColor + '"/>';
|
|
xTickEls += '<text x="' + sx(xv) + '" y="' + (h - m.bottom + 18) + '" text-anchor="middle" fill="' + textColor + '" font-size="11">' + xv + '</text>';
|
|
});
|
|
|
|
svg.innerHTML = '' +
|
|
'<rect x="0" y="0" width="520" height="280" fill="transparent" />' +
|
|
tickEls +
|
|
'<line x1="' + m.left + '" y1="' + (h - m.bottom) + '" x2="' + (w - m.right) + '" y2="' + (h - m.bottom) + '" stroke="' + axisColor + '" stroke-width="1.4" />' +
|
|
'<line x1="' + m.left + '" y1="' + m.top + '" x2="' + m.left + '" y2="' + (h - m.bottom) + '" stroke="' + axisColor + '" stroke-width="1.4" />' +
|
|
'<path d="' + path + '" fill="none" stroke="' + lineColor + '" stroke-width="2.3" />' +
|
|
pointEls +
|
|
xTickEls +
|
|
'<text x="260" y="272" text-anchor="middle" fill="' + textColor + '" font-size="12">' + options.xLabel + '</text>' +
|
|
'<text x="16" y="140" transform="rotate(-90 16 140)" text-anchor="middle" fill="' + textColor + '" font-size="12">' + options.yLabel + '</text>';
|
|
}
|
|
|
|
function drawModeBarChart(svgId) {
|
|
const svg = document.getElementById(svgId);
|
|
const modes = [
|
|
{ mode: "auto", count: 6 },
|
|
{ mode: "virtualControl", count: 6 },
|
|
{ mode: "fysicalControl", count: 4 }
|
|
];
|
|
|
|
const w = 520, h = 280;
|
|
const m = { top: 20, right: 16, bottom: 52, left: 52 };
|
|
const iw = w - m.left - m.right;
|
|
const ih = h - m.top - m.bottom;
|
|
const max = 6;
|
|
const barW = iw / modes.length * 0.58;
|
|
const gap = iw / modes.length * 0.42;
|
|
|
|
const textColor = getComputedStyle(document.documentElement).getPropertyValue("--muted").trim() || "#627489";
|
|
const axisColor = getComputedStyle(document.documentElement).getPropertyValue("--line-strong").trim() || "#96accb";
|
|
const blue = getComputedStyle(document.documentElement).getPropertyValue("--blue").trim() || "#2059d8";
|
|
const teal = getComputedStyle(document.documentElement).getPropertyValue("--teal").trim() || "#0d9f9e";
|
|
const amber = getComputedStyle(document.documentElement).getPropertyValue("--amber").trim() || "#cf8a11";
|
|
const colors = [blue, teal, amber];
|
|
|
|
let bars = "";
|
|
modes.forEach(function (d, i) {
|
|
const x = m.left + i * (barW + gap) + gap * 0.5;
|
|
const bh = (d.count / max) * ih;
|
|
const y = m.top + ih - bh;
|
|
bars += '<rect x="' + x + '" y="' + y + '" width="' + barW + '" height="' + bh + '" rx="6" fill="' + colors[i] + '" />';
|
|
bars += '<text x="' + (x + barW / 2) + '" y="' + (y - 6) + '" text-anchor="middle" fill="' + textColor + '" font-size="12">' + d.count + '</text>';
|
|
bars += '<text x="' + (x + barW / 2) + '" y="' + (h - m.bottom + 18) + '" text-anchor="middle" fill="' + textColor + '" font-size="11">' + d.mode + '</text>';
|
|
});
|
|
|
|
svg.innerHTML = '' +
|
|
'<line x1="' + m.left + '" y1="' + (h - m.bottom) + '" x2="' + (w - m.right) + '" y2="' + (h - m.bottom) + '" stroke="' + axisColor + '" stroke-width="1.4" />' +
|
|
'<line x1="' + m.left + '" y1="' + m.top + '" x2="' + m.left + '" y2="' + (h - m.bottom) + '" stroke="' + axisColor + '" stroke-width="1.4" />' +
|
|
bars +
|
|
'<text x="260" y="272" text-anchor="middle" fill="' + textColor + '" font-size="12">Mode</text>' +
|
|
'<text x="16" y="140" transform="rotate(-90 16 140)" text-anchor="middle" fill="' + textColor + '" font-size="12">Allowed actions (count)</text>';
|
|
}
|
|
|
|
function drawAllCharts() {
|
|
const ctrl = [0, 350, 550, 600, 1000];
|
|
const flow = [0, 0.9294287109, 0.941, 1.05, 1.9220000000];
|
|
const power = [5.0760000000, 39.1178710938, 64.8, 76.4, 169.2000000000];
|
|
const eff = flow.map(function (q, i) { return q / (power[i] || 1); });
|
|
|
|
drawLineChart("flowChart", {
|
|
x: ctrl,
|
|
y: flow,
|
|
xLabel: "Control setpoint",
|
|
yLabel: "Flow",
|
|
color: getComputedStyle(document.documentElement).getPropertyValue("--teal").trim() || "#0d9f9e"
|
|
});
|
|
|
|
drawLineChart("powerChart", {
|
|
x: ctrl,
|
|
y: power,
|
|
xLabel: "Control setpoint",
|
|
yLabel: "Power (kW)",
|
|
color: getComputedStyle(document.documentElement).getPropertyValue("--blue").trim() || "#2059d8"
|
|
});
|
|
|
|
drawLineChart("effChart", {
|
|
x: ctrl,
|
|
y: eff,
|
|
xLabel: "Control setpoint",
|
|
yLabel: "Efficiency index (flow/power)",
|
|
color: getComputedStyle(document.documentElement).getPropertyValue("--green").trim() || "#0fa57d"
|
|
});
|
|
|
|
drawModeBarChart("modeChart");
|
|
}
|
|
|
|
renderRows();
|
|
renderParameterSheet();
|
|
drawAllCharts();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|