chore(dashboardAPI): enforce min visual gap between threshold lines
User-visible problem: with the basin config dryRunThresholdPercent=2 (so dryRunLevel ≈ outflowLevel) and highVolumeSafetyThresholdPercent=98 (so highSafetyLevel ≈ overflowLevel), two pairs of threshold lines sat right on top of each other in the tank visual, leaving no room between them for their labels. The 'BELOW' fallback in the label algorithm couldn't fit either, so labels ended up crossing lines. Fix: enforce a minimum 28 px visual gap between adjacent threshold lines inside the tank (≈3.7 % of the 760-tall reference frame, > LABEL_H + 2). Lines closer than that get spread apart while preserving order. If the stack would push the lowest line past the tank floor, the whole stack shifts up to fit. Slight geometric distortion is accepted — the tank visual conveys ordering and zone structure, not exact-scale level measurement; numeric values are still rendered next to each line. Result: at any basin geometry, labels sit cleanly above their line with no overlap, no label-on-line collision, and no fallback to a 'stacked' position that crosses its own line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -205,11 +205,36 @@ class DashboardApi {
|
|||||||
const yFor = (v) => +(TANK_BOT - (v / heightBasin) * TANK_H).toFixed(2);
|
const yFor = (v) => +(TANK_BOT - (v / heightBasin) * TANK_H).toFixed(2);
|
||||||
const tyFor = (yLine) => +(yLine - 8).toFixed(2); // centre 16px text on the line
|
const tyFor = (yLine) => +(yLine - 8).toFixed(2); // centre 16px text on the line
|
||||||
|
|
||||||
const y_overflow = yFor(overflowLevel);
|
let y_overflow = yFor(overflowLevel);
|
||||||
const y_highSafety = yFor(highSafetyLevel);
|
let y_highSafety = yFor(highSafetyLevel);
|
||||||
const y_inflow = yFor(inflowLevel);
|
let y_inflow = yFor(inflowLevel);
|
||||||
const y_dryRun = yFor(dryRunLevel);
|
let y_dryRun = yFor(dryRunLevel);
|
||||||
const y_outflow = yFor(outflowLevel);
|
let y_outflow = yFor(outflowLevel);
|
||||||
|
|
||||||
|
// Enforce a minimum visual gap between adjacent threshold lines so labels
|
||||||
|
// can always sit cleanly between them — independent of how close the
|
||||||
|
// underlying physical thresholds are. Slight geometric distortion is
|
||||||
|
// acceptable: the tank visual conveys ORDERING and ZONE STRUCTURE, not
|
||||||
|
// exact-scale level measurement. Dashed/value labels carry the true
|
||||||
|
// numeric values.
|
||||||
|
const MIN_LINE_GAP = 28; // px (≈3.7% of 760-tall frame, > LABEL_H + 2)
|
||||||
|
const sorted = [
|
||||||
|
{ id: 'overflow', get: () => y_overflow, set: (v) => (y_overflow = v) },
|
||||||
|
{ id: 'highSafety', get: () => y_highSafety, set: (v) => (y_highSafety = v) },
|
||||||
|
{ id: 'inflow', get: () => y_inflow, set: (v) => (y_inflow = v) },
|
||||||
|
{ id: 'dryRun', get: () => y_dryRun, set: (v) => (y_dryRun = v) },
|
||||||
|
{ id: 'outflow', get: () => y_outflow, set: (v) => (y_outflow = v) },
|
||||||
|
].sort((a, b) => a.get() - b.get());
|
||||||
|
// Push down to enforce min gap (anchor: topmost line)
|
||||||
|
for (let i = 1; i < sorted.length; i++) {
|
||||||
|
const minY = sorted[i - 1].get() + MIN_LINE_GAP;
|
||||||
|
if (sorted[i].get() < minY) sorted[i].set(minY);
|
||||||
|
}
|
||||||
|
// If the last (lowest) line went past the floor, shift the whole stack up.
|
||||||
|
const overshoot = sorted[sorted.length - 1].get() - TANK_BOT;
|
||||||
|
if (overshoot > 0) {
|
||||||
|
for (const item of sorted) item.set(item.get() - overshoot);
|
||||||
|
}
|
||||||
|
|
||||||
// Label y-positions: labels sit either ABOVE or BELOW their threshold
|
// 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
|
// line, never on it. Each label is offset by ABOVE_OFFSET=22 px above
|
||||||
|
|||||||
Reference in New Issue
Block a user