diff --git a/config/pumpingStation.json b/config/pumpingStation.json index 25420f4..9edfbb2 100644 --- a/config/pumpingStation.json +++ b/config/pumpingStation.json @@ -227,14 +227,15 @@ "root": { "name": "Basin", "type": "frame", - "placement": { "left": 0, "top": 0, "width": 400, "height": 760 }, + "placement": { "left": 0, "top": 0, "right": 0, "bottom": 0 }, "background": { "color": { "fixed": "transparent" } }, "border": { "color": { "fixed": "dark-green" } }, "elements": [ { "name": "Zone Spill", "type": "rectangle", - "placement": { "top": 40, "left": 10, "width": 380, "height": {{h_spill}} }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": 5.26, "left": 2.5, "right": 2.5, "bottom": {{zb_spill}} }, "background": { "color": { "fixed": "rgba(229, 67, 67, 0.18)" } }, "border": { "color": { "fixed": "transparent" }, "width": 0 }, "config": { "text": { "mode": "fixed", "fixed": "" } } @@ -242,7 +243,8 @@ { "name": "Zone HighSafety", "type": "rectangle", - "placement": { "top": {{y_overflow}}, "left": 10, "width": 380, "height": {{h_highSafety}} }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{y_overflow}}, "left": 2.5, "right": 2.5, "bottom": {{zb_highSafety}} }, "background": { "color": { "fixed": "rgba(242, 165, 67, 0.16)" } }, "border": { "color": { "fixed": "transparent" }, "width": 0 }, "config": { "text": { "mode": "fixed", "fixed": "" } } @@ -250,7 +252,8 @@ { "name": "Zone Operating", "type": "rectangle", - "placement": { "top": {{y_highSafety}}, "left": 10, "width": 380, "height": {{h_operating}} }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{y_highSafety}}, "left": 2.5, "right": 2.5, "bottom": {{zb_operating}} }, "background": { "color": { "fixed": "rgba(95, 179, 122, 0.14)" } }, "border": { "color": { "fixed": "transparent" }, "width": 0 }, "config": { "text": { "mode": "fixed", "fixed": "" } } @@ -258,7 +261,8 @@ { "name": "Zone Dead", "type": "rectangle", - "placement": { "top": {{y_outflow}}, "left": 10, "width": 380, "height": {{h_dead}} }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{y_outflow}}, "left": 2.5, "right": 2.5, "bottom": {{zb_dead}} }, "background": { "color": { "fixed": "rgba(128, 128, 128, 0.20)" } }, "border": { "color": { "fixed": "transparent" }, "width": 0 }, "config": { "text": { "mode": "fixed", "fixed": "" } } @@ -266,7 +270,8 @@ { "name": "Tank Outline", "type": "rectangle", - "placement": { "top": 40, "left": 10, "width": 380, "height": 680 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": 5.26, "left": 2.5, "right": 2.5, "bottom": 5.27 }, "background": { "color": { "fixed": "transparent" } }, "border": { "color": { "fixed": "#8a8a8a" }, "width": 2 }, "config": { "text": { "mode": "fixed", "fixed": "" } } @@ -274,122 +279,138 @@ { "name": "Line Overflow", "type": "rectangle", - "placement": { "top": {{y_overflow}}, "left": 10, "width": 380, "height": 1 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{y_overflow}}, "left": 2.5, "right": 2.5, "bottom": {{yb_overflow}} }, "background": { "color": { "fixed": "#e54343" } }, "border": { "color": { "fixed": "#e54343" }, "width": 0 } }, { "name": "Line HighSafety", "type": "rectangle", - "placement": { "top": {{y_highSafety}}, "left": 10, "width": 380, "height": 1 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{y_highSafety}}, "left": 2.5, "right": 2.5, "bottom": {{yb_highSafety}} }, "background": { "color": { "fixed": "#f2a543" } }, "border": { "color": { "fixed": "#f2a543" }, "width": 0 } }, { "name": "Line Inflow", "type": "rectangle", - "placement": { "top": {{y_inflow}}, "left": 10, "width": 380, "height": 1 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{y_inflow}}, "left": 2.5, "right": 2.5, "bottom": {{yb_inflow}} }, "background": { "color": { "fixed": "#5fb37a" } }, "border": { "color": { "fixed": "#5fb37a" }, "width": 0 } }, { "name": "Line DryRun", "type": "rectangle", - "placement": { "top": {{y_dryRun}}, "left": 10, "width": 380, "height": 1 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{y_dryRun}}, "left": 2.5, "right": 2.5, "bottom": {{yb_dryRun}} }, "background": { "color": { "fixed": "#5b9bd5" } }, "border": { "color": { "fixed": "#5b9bd5" }, "width": 0 } }, { "name": "Line Outflow", "type": "rectangle", - "placement": { "top": {{y_outflow}}, "left": 10, "width": 380, "height": 1 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{y_outflow}}, "left": 2.5, "right": 2.5, "bottom": {{yb_outflow}} }, "background": { "color": { "fixed": "#bfbfbf" } }, "border": { "color": { "fixed": "#bfbfbf" }, "width": 0 } }, { "name": "Label Overflow Name", "type": "text", - "placement": { "top": {{ty_overflow}}, "left": 115, "width": 95, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_overflow}}, "left": 15, "right": 53, "bottom": {{tyb_overflow}} }, "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" } + "config": { "text": { "mode": "fixed", "fixed": "overflowLevel" }, "color": { "fixed": "#c92020" }, "size": 14, "align": "right", "valign": "middle" } }, { "name": "Label HighSafety Name", "type": "text", - "placement": { "top": {{ty_highSafety}}, "left": 115, "width": 95, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_highSafety}}, "left": 15, "right": 53, "bottom": {{tyb_highSafety}} }, "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" } + "config": { "text": { "mode": "fixed", "fixed": "highSafety" }, "color": { "fixed": "#cf7e20" }, "size": 14, "align": "right", "valign": "middle" } }, { "name": "Label Inflow Name", "type": "text", - "placement": { "top": {{ty_inflow}}, "left": 115, "width": 95, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_inflow}}, "left": 15, "right": 53, "bottom": {{tyb_inflow}} }, "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" } + "config": { "text": { "mode": "fixed", "fixed": "inflowLevel" }, "color": { "fixed": "#3d8a5a" }, "size": 14, "align": "right", "valign": "middle" } }, { "name": "Label DryRun Name", "type": "text", - "placement": { "top": {{ty_dryRun}}, "left": 115, "width": 95, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_dryRun}}, "left": 15, "right": 53, "bottom": {{tyb_dryRun}} }, "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" } + "config": { "text": { "mode": "fixed", "fixed": "dryRunLevel" }, "color": { "fixed": "#3a76a8" }, "size": 14, "align": "right", "valign": "middle" } }, { "name": "Label Outflow Name", "type": "text", - "placement": { "top": {{ty_outflow}}, "left": 115, "width": 95, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_outflow}}, "left": 15, "right": 53, "bottom": {{tyb_outflow}} }, "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" } + "config": { "text": { "mode": "fixed", "fixed": "outflowLevel" }, "color": { "fixed": "#6a6a6a" }, "size": 14, "align": "right", "valign": "middle" } }, { "name": "Value Overflow", "type": "metric-value", - "placement": { "top": {{ty_overflow}}, "left": 215, "width": 80, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_overflow}}, "left": 53, "right": 12, "bottom": {{tyb_overflow}} }, "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" } + "config": { "text": { "mode": "field", "fixed": "", "field": "overflowLevel" }, "color": { "fixed": "#c92020" }, "size": 14, "align": "left", "valign": "middle" } }, { "name": "Value HighSafety", "type": "metric-value", - "placement": { "top": {{ty_highSafety}}, "left": 215, "width": 80, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_highSafety}}, "left": 53, "right": 12, "bottom": {{tyb_highSafety}} }, "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" } + "config": { "text": { "mode": "field", "fixed": "", "field": "highVolumeSafetyLevel" }, "color": { "fixed": "#cf7e20" }, "size": 14, "align": "left", "valign": "middle" } }, { "name": "Value Inflow", "type": "metric-value", - "placement": { "top": {{ty_inflow}}, "left": 215, "width": 80, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_inflow}}, "left": 53, "right": 12, "bottom": {{tyb_inflow}} }, "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" } + "config": { "text": { "mode": "field", "fixed": "", "field": "inflowLevel" }, "color": { "fixed": "#3d8a5a" }, "size": 14, "align": "left", "valign": "middle" } }, { "name": "Value DryRun", "type": "metric-value", - "placement": { "top": {{ty_dryRun}}, "left": 215, "width": 80, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_dryRun}}, "left": 53, "right": 12, "bottom": {{tyb_dryRun}} }, "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" } + "config": { "text": { "mode": "field", "fixed": "", "field": "dryRunLevel" }, "color": { "fixed": "#3a76a8" }, "size": 14, "align": "left", "valign": "middle" } }, { "name": "Value Outflow", "type": "metric-value", - "placement": { "top": {{ty_outflow}}, "left": 215, "width": 80, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": {{ty_outflow}}, "left": 53, "right": 12, "bottom": {{tyb_outflow}} }, "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" } + "config": { "text": { "mode": "field", "fixed": "", "field": "outflowLevel" }, "color": { "fixed": "#6a6a6a" }, "size": 14, "align": "left", "valign": "middle" } }, { "name": "Header Rim", "type": "text", - "placement": { "top": 20, "left": 10, "width": 380, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": 2.63, "left": 2.5, "right": 2.5, "bottom": 95.26 }, "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 +418,8 @@ { "name": "Footer Floor", "type": "text", - "placement": { "top": 724, "left": 10, "width": 380, "height": 16 }, + "constraint": { "horizontal": "scale", "vertical": "scale" }, + "placement": { "top": 95.26, "left": 2.5, "right": 2.5, "bottom": 2.63 }, "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" } diff --git a/src/specificClass.js b/src/specificClass.js index 3ada38e..6c58a7b 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -189,11 +189,19 @@ class DashboardApi { const dryRunLevel = outflowLevel * (1 + dryRunPct / 100); const highSafetyLevel = overflowLevel * (highPct / 100); - // 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. + // Reference frame: 400 (logical w) x 760 (logical h) px. With every + // canvas element using `constraint: { horizontal: scale, vertical: scale }`, + // Grafana interprets placement values as PERCENTAGES of the panel size, + // not pixels — so the basin stretches to fill the card at any viewport + // and stays centered without letterboxing. + // Tank reference: rim at y=40px (5.26%), floor at y=720px (94.74%), + // centred vertically with 40px top/bottom margins for rim/floor labels. + const FRAME_W = 400, FRAME_H = 760; const TANK_TOP = 40, TANK_BOT = 720, TANK_H = TANK_BOT - TANK_TOP; + const yp = (v) => +(v / FRAME_H * 100).toFixed(2); + const xp = (v) => +(v / FRAME_W * 100).toFixed(2); + const hp = (v) => +(v / FRAME_H * 100).toFixed(2); + const wp = (v) => +(v / FRAME_W * 100).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 @@ -243,6 +251,17 @@ class DashboardApi { } const ty = Object.fromEntries(lines.map((l) => [l.id, +l.y.toFixed(2)])); + // Canvas elements use `constraint: { horizontal: scale, vertical: scale }` + // with margin-style placement (top + bottom + left + right, all %s of the + // panel). Bottom = % from panel bottom, top = % from panel top. Width and + // height are derived as 100 - top - bottom, etc. + // We emit *all* placement margins precomputed so the JSON template stays + // declarative. + const LABEL_H_PCT = hp(16); // 16 px label height as % of frame + const LINE_H_PCT = hp(1); // 1 px line height as % of frame + const bMargin = (top, h) => +(100 - top - h).toFixed(2); + const lineBottom = (lineY) => +(100 - yp(lineY) - LINE_H_PCT).toFixed(2); + const labelBottom = (lblY) => +(100 - yp(lblY) - LABEL_H_PCT).toFixed(2); return { heightBasin: +heightBasin.toFixed(2), outflowLevel: +outflowLevel.toFixed(3), @@ -250,16 +269,34 @@ class DashboardApi { overflowLevel: +overflowLevel.toFixed(3), dryRunLevel: +dryRunLevel.toFixed(3), highSafetyLevel: +highSafetyLevel.toFixed(3), - y_overflow, y_highSafety, y_inflow, y_dryRun, y_outflow, - h_spill: +(y_overflow - TANK_TOP).toFixed(2), - h_highSafety: +(y_highSafety - y_overflow).toFixed(2), - h_operating: +(y_outflow - y_highSafety).toFixed(2), - h_dead: +(TANK_BOT - y_outflow).toFixed(2), - ty_overflow: ty.overflow, - ty_highSafety: ty.highSafety, - ty_inflow: ty.inflow, - ty_dryRun: ty.dryRun, - ty_outflow: ty.outflow, + // Threshold line top margins (% from panel top) + y_overflow: yp(y_overflow), + y_highSafety: yp(y_highSafety), + y_inflow: yp(y_inflow), + y_dryRun: yp(y_dryRun), + y_outflow: yp(y_outflow), + // Threshold line bottom margins (% from panel bottom) + yb_overflow: lineBottom(y_overflow), + yb_highSafety: lineBottom(y_highSafety), + yb_inflow: lineBottom(y_inflow), + yb_dryRun: lineBottom(y_dryRun), + yb_outflow: lineBottom(y_outflow), + // Zone bottom margins (zones end at the next line below) + zb_spill: +(100 - yp(y_overflow)).toFixed(2), // ends at overflow line + zb_highSafety: +(100 - yp(y_highSafety)).toFixed(2), // ends at highSafety line + zb_operating: +(100 - yp(y_outflow)).toFixed(2), // ends at outflow line + zb_dead: +(100 - yp(TANK_BOT)).toFixed(2), // ends at floor + // Label top margins (% from panel top) and bottom margins (% from panel bottom) + ty_overflow: yp(ty.overflow), + ty_highSafety: yp(ty.highSafety), + ty_inflow: yp(ty.inflow), + ty_dryRun: yp(ty.dryRun), + ty_outflow: yp(ty.outflow), + tyb_overflow: labelBottom(ty.overflow), + tyb_highSafety: labelBottom(ty.highSafety), + tyb_inflow: labelBottom(ty.inflow), + tyb_dryRun: labelBottom(ty.dryRun), + tyb_outflow: labelBottom(ty.outflow), }; }