chore(dashboardAPI): center tank vertically + handle floor-edge labels

Tank rectangle moved from top-aligned (top=20 in 760 frame) to vertically
centered (top=40, with 40 px top + 40 px bottom margins for the rim and
floor caption text). Header rim caption shifted to y=20, footer floor to
y=724, so both sit just outside the tank rect.

Label algorithm extended: when a label would normally go BELOW its line
but doing so would push it past the tank floor (which happens for very
small dryRunThresholdPercent — dryRunLevel sits right on outflowLevel,
both nearly at the basin floor), it falls back to stacking ABOVE the
previous label instead of extending into invisible space. This keeps
all 5 threshold labels inside the visible canvas area at the cost of a
slight visual overlap of the lowest label with its own line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-28 11:38:58 +02:00
parent 41a20d4679
commit 193f913eb1
2 changed files with 27 additions and 14 deletions

View File

@@ -234,7 +234,7 @@
{
"name": "Zone Spill",
"type": "rectangle",
"placement": { "top": 20, "left": 10, "width": 380, "height": {{h_spill}} },
"placement": { "top": 40, "left": 10, "width": 380, "height": {{h_spill}} },
"background": { "color": { "fixed": "rgba(229, 67, 67, 0.18)" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "fixed", "fixed": "" } }
@@ -266,7 +266,7 @@
{
"name": "Tank Outline",
"type": "rectangle",
"placement": { "top": 20, "left": 10, "width": 380, "height": 680 },
"placement": { "top": 40, "left": 10, "width": 380, "height": 680 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "#8a8a8a" }, "width": 2 },
"config": { "text": { "mode": "fixed", "fixed": "" } }
@@ -389,7 +389,7 @@
{
"name": "Header Rim",
"type": "text",
"placement": { "top": 2, "left": 10, "width": 380, "height": 16 },
"placement": { "top": 20, "left": 10, "width": 380, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "fixed", "fixed": "rim ({{heightBasin}} m)" }, "color": { "fixed": "#8a8a8a" }, "size": 10, "align": "center", "valign": "middle" }
@@ -397,7 +397,7 @@
{
"name": "Footer Floor",
"type": "text",
"placement": { "top": 728, "left": 10, "width": 380, "height": 16 },
"placement": { "top": 724, "left": 10, "width": 380, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "fixed", "fixed": "floor (0.00 m)" }, "color": { "fixed": "#8a8a8a" }, "size": 10, "align": "center", "valign": "middle" }

View File

@@ -189,12 +189,11 @@ class DashboardApi {
const dryRunLevel = outflowLevel * (1 + dryRunPct / 100);
const highSafetyLevel = overflowLevel * (highPct / 100);
// Canvas tank: rim at y=20px, floor at y=700px (680px tall). Must match
// hard-coded tank rectangle placement in config/pumpingStation.json
// (basin row is h:20 grid rows; canvas root frame is 400x760 px — taller
// than wide to match the card's aspect ratio so the tank fills the card
// vertically with no letterboxing).
const TANK_TOP = 20, TANK_BOT = 700, TANK_H = TANK_BOT - TANK_TOP;
// Canvas tank: rim at y=40px, floor at y=720px (680px tall). Centered
// vertically in a 760px tall frame with 40px top/bottom margins for the
// header ('rim (X m)') and footer ('floor (0.00 m)') labels. Must match
// hard-coded tank rectangle placement in config/pumpingStation.json.
const TANK_TOP = 40, TANK_BOT = 720, TANK_H = TANK_BOT - TANK_TOP;
const yFor = (v) => +(TANK_BOT - (v / heightBasin) * TANK_H).toFixed(2);
const tyFor = (yLine) => +(yLine - 8).toFixed(2); // centre 16px text on the line
@@ -213,9 +212,10 @@ class DashboardApi {
// basin (dryRun=2% means dryRunLevel sits right on outflowLevel; high-
// Safety=98% puts it just under overflowLevel) this naturally puts
// highSafety BELOW and outflow BELOW.
const ABOVE_OFFSET = 22; // label_top = line_y - 22 (label bottom is 6 px clear above the line)
const BELOW_OFFSET = 6; // label_top = line_y + 6 (label is 6 px clear below the line)
const MIN_DIST_FOR_ABOVE = 24; // if distance to previous (upper) line < this, go below
const LABEL_H = 16;
const ABOVE_OFFSET = 22; // label_top = line_y - 22 (6 px clear above line)
const BELOW_OFFSET = 6; // label_top = line_y + 6 (6 px clear below line)
const MIN_DIST_FOR_ABOVE = 24; // if distance to upper line < this, try below
const lines = [
{ id: 'overflow', line: y_overflow },
{ id: 'highSafety', line: y_highSafety },
@@ -226,7 +226,20 @@ class DashboardApi {
for (let i = 0; i < lines.length; i++) {
const prev = i > 0 ? lines[i - 1] : null;
const tooClose = prev && (lines[i].line - prev.line) < MIN_DIST_FOR_ABOVE;
lines[i].y = tooClose ? (lines[i].line + BELOW_OFFSET) : (lines[i].line - ABOVE_OFFSET);
if (tooClose) {
// Default to BELOW unless the label would be clipped by the tank
// floor (thresholds at the very bottom — dryRun=tiny% means
// dryRunLevel sits right on the floor). Then stack ABOVE the
// previous label instead, even if it slightly crowds its own line.
const belowY = lines[i].line + BELOW_OFFSET;
if (belowY + LABEL_H <= TANK_BOT) {
lines[i].y = belowY;
} else {
lines[i].y = prev.y + LABEL_H + 2; // stack above with 2 px gap
}
} else {
lines[i].y = lines[i].line - ABOVE_OFFSET;
}
}
const ty = Object.fromEntries(lines.map((l) => [l.id, +l.y.toFixed(2)]));