chore(dashboardAPI): center basin labels, position above/below lines

Threshold labels were sitting right on top of their lines (label center
at line_y - 8) and were right-aligned at the tank's right edge. They now:

- Sit clearly above the line (label bottom 6 px above) by default, or
  below the line (label top 6 px below) when an adjacent threshold is
  closer than 24 px (would crowd both labels above their lines). For
  the current basin config this puts overflowLevel + inflowLevel +
  dryRunLevel ABOVE their lines, and highSafety + outflowLevel BELOW.
- Are centered horizontally in the tank (name at left:115 width:95
  right-aligned, value at left:215 width:80 left-aligned) so the
  combined phrase "overflowLevel  3.22 m" reads as one centered string.

Value width 60 → 80 so 'mm'-formatted small-meter values don't wrap to
two lines. Footer floor moved to y:728 to keep clear of the BELOW labels
near the tank floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-28 11:32:45 +02:00
parent 8a26e17780
commit 41a20d4679
2 changed files with 35 additions and 30 deletions

View File

@@ -309,7 +309,7 @@
{
"name": "Label Overflow Name",
"type": "text",
"placement": { "top": {{ty_overflow}}, "left": 180, "width": 140, "height": 16 },
"placement": { "top": {{ty_overflow}}, "left": 115, "width": 95, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "fixed", "fixed": "overflowLevel" }, "color": { "fixed": "#c92020" }, "size": 11, "align": "right", "valign": "middle" }
@@ -317,7 +317,7 @@
{
"name": "Label HighSafety Name",
"type": "text",
"placement": { "top": {{ty_highSafety}}, "left": 180, "width": 140, "height": 16 },
"placement": { "top": {{ty_highSafety}}, "left": 115, "width": 95, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "fixed", "fixed": "highSafety" }, "color": { "fixed": "#cf7e20" }, "size": 11, "align": "right", "valign": "middle" }
@@ -325,7 +325,7 @@
{
"name": "Label Inflow Name",
"type": "text",
"placement": { "top": {{ty_inflow}}, "left": 180, "width": 140, "height": 16 },
"placement": { "top": {{ty_inflow}}, "left": 115, "width": 95, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "fixed", "fixed": "inflowLevel" }, "color": { "fixed": "#3d8a5a" }, "size": 11, "align": "right", "valign": "middle" }
@@ -333,7 +333,7 @@
{
"name": "Label DryRun Name",
"type": "text",
"placement": { "top": {{ty_dryRun}}, "left": 180, "width": 140, "height": 16 },
"placement": { "top": {{ty_dryRun}}, "left": 115, "width": 95, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "fixed", "fixed": "dryRunLevel" }, "color": { "fixed": "#3a76a8" }, "size": 11, "align": "right", "valign": "middle" }
@@ -341,7 +341,7 @@
{
"name": "Label Outflow Name",
"type": "text",
"placement": { "top": {{ty_outflow}}, "left": 180, "width": 140, "height": 16 },
"placement": { "top": {{ty_outflow}}, "left": 115, "width": 95, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "fixed", "fixed": "outflowLevel" }, "color": { "fixed": "#6a6a6a" }, "size": 11, "align": "right", "valign": "middle" }
@@ -349,7 +349,7 @@
{
"name": "Value Overflow",
"type": "metric-value",
"placement": { "top": {{ty_overflow}}, "left": 323, "width": 65, "height": 16 },
"placement": { "top": {{ty_overflow}}, "left": 215, "width": 80, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "field", "fixed": "", "field": "overflowLevel" }, "color": { "fixed": "#c92020" }, "size": 11, "align": "left", "valign": "middle" }
@@ -357,7 +357,7 @@
{
"name": "Value HighSafety",
"type": "metric-value",
"placement": { "top": {{ty_highSafety}}, "left": 323, "width": 65, "height": 16 },
"placement": { "top": {{ty_highSafety}}, "left": 215, "width": 80, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "field", "fixed": "", "field": "highVolumeSafetyLevel" }, "color": { "fixed": "#cf7e20" }, "size": 11, "align": "left", "valign": "middle" }
@@ -365,7 +365,7 @@
{
"name": "Value Inflow",
"type": "metric-value",
"placement": { "top": {{ty_inflow}}, "left": 323, "width": 65, "height": 16 },
"placement": { "top": {{ty_inflow}}, "left": 215, "width": 80, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "field", "fixed": "", "field": "inflowLevel" }, "color": { "fixed": "#3d8a5a" }, "size": 11, "align": "left", "valign": "middle" }
@@ -373,7 +373,7 @@
{
"name": "Value DryRun",
"type": "metric-value",
"placement": { "top": {{ty_dryRun}}, "left": 323, "width": 65, "height": 16 },
"placement": { "top": {{ty_dryRun}}, "left": 215, "width": 80, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "field", "fixed": "", "field": "dryRunLevel" }, "color": { "fixed": "#3a76a8" }, "size": 11, "align": "left", "valign": "middle" }
@@ -381,7 +381,7 @@
{
"name": "Value Outflow",
"type": "metric-value",
"placement": { "top": {{ty_outflow}}, "left": 323, "width": 65, "height": 16 },
"placement": { "top": {{ty_outflow}}, "left": 215, "width": 80, "height": 16 },
"background": { "color": { "fixed": "transparent" } },
"border": { "color": { "fixed": "transparent" }, "width": 0 },
"config": { "text": { "mode": "field", "fixed": "", "field": "outflowLevel" }, "color": { "fixed": "#6a6a6a" }, "size": 11, "align": "left", "valign": "middle" }
@@ -397,7 +397,7 @@
{
"name": "Footer Floor",
"type": "text",
"placement": { "top": 702, "left": 10, "width": 380, "height": 16 },
"placement": { "top": 728, "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

@@ -204,26 +204,31 @@ class DashboardApi {
const y_dryRun = yFor(dryRunLevel);
const y_outflow = yFor(outflowLevel);
// Label y-positions get min-gap enforcement so labels never overlap even
// when thresholds sit nearly on top of each other (e.g. dryRun=2 % means
// dryRunLevel sits right on outflowLevel; highSafety=98 % puts it under
// overflow). Lines stay at proportional y; only the label text moves.
// Two-pass (down + up) mirrors editor's basin-diagram.js placement logic.
const GAP = 20;
const labels = [
{ id: 'overflow', y: tyFor(y_overflow) },
{ id: 'highSafety', y: tyFor(y_highSafety) },
{ id: 'inflow', y: tyFor(y_inflow) },
{ id: 'dryRun', y: tyFor(y_dryRun) },
{ id: 'outflow', y: tyFor(y_outflow) },
].sort((a, b) => a.y - b.y);
for (let i = 1; i < labels.length; i++) {
if (labels[i].y < labels[i - 1].y + GAP) labels[i].y = labels[i - 1].y + GAP;
// Label y-positions: labels sit either ABOVE or BELOW their threshold
// line, never on it. Each label is offset by ABOVE_OFFSET=22 px above
// its line by default (16 px tall label + 6 px clear above the line).
// If two thresholds are too close together for both labels to fit ABOVE
// their lines (label of the lower one would cross the upper line), the
// lower one's label flips BELOW its line instead. With the current
// 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 lines = [
{ id: 'overflow', line: y_overflow },
{ id: 'highSafety', line: y_highSafety },
{ id: 'inflow', line: y_inflow },
{ id: 'dryRun', line: y_dryRun },
{ id: 'outflow', line: y_outflow },
].sort((a, b) => a.line - b.line);
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);
}
for (let i = labels.length - 2; i >= 0; i--) {
if (labels[i].y > labels[i + 1].y - GAP) labels[i].y = labels[i + 1].y - GAP;
}
const ty = Object.fromEntries(labels.map((l) => [l.id, +l.y.toFixed(2)]));
const ty = Object.fromEntries(lines.map((l) => [l.id, +l.y.toFixed(2)]));
return {
heightBasin: +heightBasin.toFixed(2),