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:
@@ -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" }
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user