Skip to main content

Water Balance

An interactive flow diagram that accounts for water moving through a site — sources, tanks, treatment, consumption, and recycled loops — rendered with React Flow. It answers "where did the water go, and does it add up?"

At a glance

Route/water_balance/graphWaterBalance (libs/waterBalance/src/WaterBalancePage.jsx)
Sidebar labelWater Mass Balance — a top-level item in the main nav section (not nested under Efficiency, despite the route grouping)
Page heading"Water Balance Diagram"
PermissionWATER_BALANCE
Without the permissionThe sidebar item points at /feature_locked/WATER_BALANCE/WATER_BALANCE — a marketing page served by libs/featureLocked
Available inproduction demo — routed in these two apps only
Librarylibs/waterBalance/ — standard dataSource → controller → store → components layering
Data endpointGET waterBalance/data (the only one)
Refresh5 min — only while viewing today
Diagram enginereactflow v11 (legacy package name)
ExportPNG, client-side via html-to-image
What Water Balance does NOT do
  • It does not compute anything. Node types, values, positions, edge paths, and the balance percentage all arrive from the backend; the frontend only maps type strings to components.
  • It does not produce the water-balance report. That is a separate server-rendered document — see Reports.
  • It does not export data. The download is a PNG image of the diagram; there is no table or spreadsheet export.
  • It is not available in the standalone apps. The water_balance paths in uwms, rwi, and lakepulse are the report sub-route (/reports/water_balance), not this diagram.
  • It does not poll continuously — auto-refresh only runs while you are viewing today.

The physical flow

The diagram wires typed nodes (tanks, pumps, valves, processes, meters) together with typed edges (raw vs recycled flows). A simplified site:

Summary: Solid lines are raw water, dashed lines are recycled water (per the in-app legend). Consumption flows carry animated arrows. Node types, values, and layout all come from the backend — the frontend only maps each type string to a component.


How a response becomes a diagram

Summary: One fetch fills the store; page and view are pure client-side selectors into that payload. The key on ReactFlow means switching either one remounts the whole graph rather than diffing it.


Page chrome & controls

ControlWhereWhat it doesOn change
Granularity selectHeaderDay (HOUR) or Month (DATE)Refetches. Choosing Day also resets the date to today
Date pickerHeaderAppDatePickerSelection — day or month mode follows the selectRefetches
TodayHeaderTodayDate button — jumps to today + HOURRefetches
SearchHeaderShared SearchComponent
View toggleGraph, top-left panelOverview / DetailedClient-side; remounts the graph
Page dropdownGraph, top-left panelSite/section picker, labelled by each page's displayNameClient-side; remounts the graph
Zoom / fitGraph, bottom-rightStandard React Flow controls (the interactive toggle is hidden)
InfoGraph, bottom-rightToggles the legend panel
DownloadGraph, bottom-rightOpens the export dialog

Before data arrives the header renders a disabled placeholder select plus a disabled CustomSingleDatePicker; once balanceData.data exists it swaps to the live granularity select and AppDatePickerSelection. See Components for which date pickers are standard vs legacy.


Two views, two granularities

ControlOptionsBackend keyRefetch?
View (WaterBalanceGraphType)Overview (SUMMARY) · Detailed (DETAIL)summary / diagramNo
Granularity (WaterBalanceDateType)Day (HOUR) · Month (DATE)— (query param type)Yes
Page (selectedPageId)Whatever keys the response carries; physical is always presentNo

Each view also selects its own legend via infoMenuKey (SUMMARY_INFO / DETAIL_INFO).


Node types

The backend gives each node a type string; WaterBalanceGraphComponents.jsx maps it to a component (nodeTypes).

Detailed view — the physical diagram

typeComponentRepresentsWhat it showsAlert stateOn click
CARDConsumptionNodeConsumption / meter readingValue, label (tooltip on hover, clamped to 2 lines)Blinks red + red label bar when meta.limitReached; adds a (subTitle unit) line. Greyscaled with lastUpdatedTime when offlineOpens that unit's legacy source page in a new tab
TANKWaterTankNodeStorage tankAnimated fill (shared WaterTank), value, labelBlinks red when meta.thresholdReached; greyscaled + lastUpdatedTime when offlineOpens the stock page for that unit in a new tab
OVALOvalNodeSource / output labelValue + label in a pill
PROCESSProcessNodeTreatment / process stepValue + label in a filled box, white text
PUMPPumpNodePumpStatic icon only — no data at all
VALVEValveNodeValve / flow meterStatic icon only — no data at all

Tank threshold lines, the empty line, and the threshold percentages only render at zoom ≥ 0.75 (a useStore selector on the transform).

Overview — summary tiles

typeComponentRepresentsWhat it showsAlert stateOn click
SUMMARY_DATASummaryDataNodeSingle metric tilevalue siUnit + label; formula tooltip via a ? icon
SUMMARY_DETAILSummaryDetailNodeGrouped metricvalue siUnit + label (count); drawn as a stack of cards; formula tooltipBlinks red if any member unit has meta.limitReachedToggles a popup listing every member unit
WATER_BALANCEWaterBalanceNodeHeadline balance figurevalue % (large) + formula tooltip
SUMMARY_DIFFERENCESummaryDifferenceNodeReconciliation figurelabel : value in a pill — no unit

The ? formula icon is hidden whenever data.formula is empty.

Every node draws four source + four target Handles (one per side), all styled transparent and borderless so they are invisible, and the graph uses connectionMode="loose" so edges can attach anywhere. All node components are React.memo.


Edge types

typeComponentPathAppearanceMeaning
customFlowEdgePoly-line built from data.controlPointsSolidRaw water on a hand-routed path
stepCustomStepEdgegetSmoothStepPathSolidRaw water, grid-routed
dashStepCustomDashEdgegetSmoothStepPathDashed 12 12Recycled water
  • Default stroke is #006183 at width 2; a per-edge style from the backend overrides it.
  • Edges with data.isConsumption overlay AnimatedArrow — arrow glyphs that travel the path via SVG animateMotion. Duration is derived from the path length (Euclidean for custom, Manhattan for the step edges).
  • In the export dialog, custom and step are both swapped for CustomStepEdgeWithoutAnimation so the still frame is clean; dashStep is kept as-is (CustomDownloadEdgeType).

Click-through navigation

All node navigation goes through the shared useNavigateSearchParams hook, whose openInNewTab defaults to true — so every one of these opens a new tab.

FromTargetCarries
CARD node/monitoring/source_category/SOURCE_CATEGORY/{subCategoryId}/{unitId}navDate, navType
TANK node/monitoring/stock_category/STOCK_CATEGORY?unitId={unitId}navDate, navType
Summary popup row, with meta.maxCapacityStock page for that unitnavDate, navType
Summary popup row, without meta.maxCapacityLegacy source page for that unitnavDate, navType

The hook also writes the date and granularity into LocalDB (commonDate, commonDateType) and sets constantDate / constantDateType on the global AppStore. That is the other half of the loop: when the app supplies a constantDate, Water Balance mirrors it into params.date1 and refetches — so a date chosen here follows you into the unit page, and a date set elsewhere follows you back.


Data & APIs

The data endpoint

EndpointGET waterBalance/data (Urls.waterBalanceData)
Paramsdate1 (DD/MM/YYYY), type (HOUR | DATE)
Fetched onMount · date change · granularity change · constantDate change · the 5-min interval
Not fetched onView toggle · page dropdown — both read from the store

constants.refreshDuration is 5 minutes, and the interval is only created when DateFormatter.isSame(params.date1) says the selected date is today.

Full response shape
{
"data": {
"pages": {
"physical": {
"id": "physical",
"displayName": "Physical", // label in the page dropdown
"siUnit": "kl", // read from the physical page only
"summary": { // Overview view
"nodes": [ /* SUMMARY_* + WATER_BALANCE nodes */ ],
"edges": [ /* typed edges */ ]
},
"diagram": { // Detailed view
"nodes": [ /* CARD / TANK / OVAL / PROCESS / PUMP / VALVE */ ],
"edges": [ /* typed edges */ ]
}
}
// ...further pages, keyed by id
}
}
}

The store reads siUnit from pages.physical only — every page's values are rendered with that one unit. kl is the component-level default.

Node data contract, field by field

Standard React Flow node fields (id, type, position) come straight from the backend. Everything below lives under data:

FieldUsed byPurpose
labelallDisplay name; tooltipped where it can overflow
valueall but PUMP/VALVEThe reading
onlineCARD, TANKfalse greyscales the node and reveals lastUpdatedTime
lastUpdatedTimeCARD, TANKShown only while offline
unitIdCARD, TANK, popup rowsNavigation target
subCategoryIdCARD, popup rowsNavigation target
formulathe four summary typesTooltip text; the ? icon hides when empty
graphNodes[]SUMMARY_DETAILMember units — each with displayName, value, unitId, subCategoryId, meta.limitReached, meta.maxCapacity
meta.limitReachedCARD, popup rowsDrives the red/blink state
meta.subTitleCARDExtra line rendered only when the limit is reached
meta.percentageTANKFill level
meta.thresholdReachedTANKDrives the red/blink state
meta.thresholdValue.fullPercentageTANKUpper threshold line + label
meta.thresholdValue.emptyPercentageTANKLower threshold line + label
meta.maxCapacitypopup rowsPresence routes to the stock page instead of the source page
meta.backgroundColorSUMMARY_DETAILPopup header colour
style.displayTextColorCARD, TANKValue colour while online
style.backgroundColorTANKWave + border colour
style.backgroundOVAL, PROCESSFill colour

Edge data:

FieldUsed byPurpose
controlPoints[]custom{x, y} waypoints for the poly-line
isConsumptionall threeOverlays the animated arrows

Analytics

EventWhen
PAGE_VIEWPage mount
WATER_BALANCE_DATE_CHANGEDate or granularity actually changes (payload = the new params)

Both no-op when analytics is off — the backwards analyticsEnv flag, see Configuration & Security.

State

WaterBalanceStore is a React Context (WaterBalanceDataProvider / WaterBalanceDataContext) exposing balanceData ({ data, siUnit }), graphType, params, isLoading, fullScreenLoader, selectedPageId, openInfoPopup, openDownloadPopup, and downloadContainerRef. On mount the page also calls setMenuIconEnabled(false) and setSelectedCategory(NavigationOptions.waterBalance) on the global store. See State Management and API Call Flow.


Render states

Summary: A failed request and a site with no diagram configured land in the same place — the "contact us" overlay. There is no distinct error state.

StateWhat the user sees
LoadingCentred loader in place of the graph; the view toggle is disabled and the export dialog is not mounted
No diagram configuredContactForDiagram overlay: a warning icon and "Please reach out to us through 'Issue Reporting form' to get your Water Balance Summary made." It makes no API or mail call
Fetch failedIdentical to the above — the error is only console.error'd
No data at allThe info/legend popup auto-opens once loading finishes
Offline unitNode greyscaled, value greyed, lastUpdatedTime shown
Limit / threshold reachedNode blinks red (2 s loop) and recolours

Export

The download control opens a dialog holding a static copy of the current graph: selection, dragging, connecting, zoom, and panning are all disabled, maxZoom is clamped to 1, and fitView frames it. Above it sits the title and a read-only date picker (dimmed, pointerEvents: none) so the exported image carries its date.

HowDownload.PNG in libs/shared/src/utils/downloadUtil.jshtml-to-image toPng
TargetThe dialog's container ref (downloadContainerRef)
OptionspixelRatio: 8, skipFonts: true, cacheBust: true, <style> nodes filtered out
DeliveryData URL on a synthetic <a download> that is clicked
FilenameDay: {date1}_{pageId}_{username} · Month: {MMM/YYYY}_{pageId}_{username}
While capturingA full-screen backdrop + spinner (fullScreenLoader)

The export is an image only — no data-table or spreadsheet export is wired up.


Info popup / legend

Titled "Balance Diagram Info", contents keyed off the active view.

Overview (SUMMARY_INFO)Detailed (DETAIL_INFO)
Combined · Difference · Zoom In/Out (Ctrl + scroll)Raw Water · Recycled Water · Limit Reached · 90% Filled · Storage Tank · No Flow Meter · Pump · Valve

React Flow version

Built on reactflow v11 — the legacy package name. Imports come from 'reactflow': ReactFlow, Handle, Position, BaseEdge, getSmoothStepPath, useStore, Background, Controls, ControlButton, Panel.

Going forward

React Flow v12 ships as the renamed @xyflow/react package. An upgrade would swap the import path and adopt its minor API changes. Until then keep new water-balance work on the v11 reactflow imports so the graph stays consistent.


Visibility & access

Which apps register the route

AppPathRenders
production/water_balance/graphWaterBalance
demo/water_balance/graphWaterBalance
uwms · rwi · lakepulseNot registered. Their water_balance path is the report sub-route

Route-level gating: none

In both apps the route element is a bare <WaterBalance /> — no PermissionWrapper. Hitting the URL directly renders the page regardless of permission. The intended gate is the sidebar.

PropertyValue
Nav keyWATER_BALANCE
Display nameWater Mass Balance
PlacementTop level of the main nav section, beside Metrics and Efficiency
permissionIdWATER_BALANCE
lockedPath/feature_locked/WATER_BALANCE/WATER_BALANCE
isDemoOptionfalse — visible in every build

Without the permission the item routes to the feature-locked marketing page rather than disappearing. See Permissions → Sidebar access modes.

Service and subscription gating

This feature performs no checkServiceExist check — unlike the Dashboard cards, it is either permitted or locked. Subscription expiry is handled globally by the app-wide overlay, not here.


Dependencies

Shared store & services

ImportUsed for
AppStoreconstantDate sync, setMenuIconEnabled, setSelectedCategory
apiClient · UrlsThe single waterBalance/data call
AnalyticsService · AnalyticEventsPAGE_VIEW, WATER_BALANCE_DATE_CHANGE
LocalDBInstance · LocalDBKeysReading the username for the export filename
DownloadDownload.PNG for the image export
DateFormatterisSame — the today-only refresh check
FormattervalueFormatter, defaultValue
NavigationHelper · useNavigateSearchParamsNode click-through

Component library

SubPageWrapper, FixedBar, If / IfNot, Expanded, CustomLoader, WaterTank, AppDatePickerSelection, CustomSingleDatePicker (disabled placeholder only), TodayDate, SearchComponent, AppTooltip, AppButton.

Lib-internal

WaterBalanceDataContext / WaterBalanceDataProvider, waterBalanceEnums, markerStyle, and the BalanceNodeType / CustomEgdeType / CustomDownloadEdgeType maps.

External npm

reactflow (v11), html-to-image, @mui/material, @emotion/react (blink keyframes), @mui/icons-material, @iconify/react, moment, lodash. Exact versions live in package.json.


Usage examples

Read the current graph from the store

import { useContext } from 'react';
import { WaterBalanceDataContext } from '@aquagen-mf-webapp/waterBalance/store/WaterBalanceStore';
import { WaterBalanceGraphType } from '@aquagen-mf-webapp/waterBalance/enums/waterBalanceEnums';

function NodeCount() {
const { balanceData, selectedPageId, graphType, isLoading } = useContext(
WaterBalanceDataContext
);
if (isLoading) return null;

const key = WaterBalanceGraphType[graphType].responseKey; // 'summary' | 'diagram'
const nodes = balanceData.data?.[selectedPageId]?.[key]?.nodes ?? [];
return <span>{nodes.length} nodes in {balanceData.siUnit}</span>;
}

Switch view or page programmatically

const { setGraphType, setSelectedPageId, setParams, params } = useContext(
WaterBalanceDataContext
);

setGraphType(WaterBalanceGraphType.DETAIL.id); // client-side, remounts the graph
setSelectedPageId('physical'); // client-side, remounts the graph
setParams({ ...params, type: 'DATE' }); // triggers a refetch

Fire the feature's analytics events

import { AnalyticsService } from '@aquagen-mf-webapp/shared/services';
import { AnalyticEvents } from '@aquagen-mf-webapp/shared/enums';

AnalyticsService.sendEvent(AnalyticEvents.PAGE_VIEW, {}, true); // third arg on mount
AnalyticsService.sendEvent(AnalyticEvents.WATER_BALANCE_DATE_CHANGE, newParams);

Troubleshooting

"Please reach out to us…" appears even though the site has a diagram

The overlay renders whenever the current page + view has zero nodes — check pages[selectedPageId].summary and .diagram separately. A silently failed request produces the same screen, so check the console for Error Fetching Water Balance Data before assuming the site is unconfigured.

A meter reads -- but the unit is definitely reporting

A genuine 0 is falsy, and both the consumption card and the tank test the value with a falsy check. -- therefore means either no data or exactly zero.

The graph stopped updating

The 5-minute interval is only created when the selected date is today. Viewing any past date is intentionally static.

The page dropdown lost a site after a refresh

Its option list is memoised with an empty dependency array, so the 5-minute refresh does not rebuild it. Switching view or page remounts React Flow, which does.

Clicking a node does nothing

Only CARD, TANK, and summary-popup rows navigate. OVAL, PROCESS, PUMP, and VALVE have no handler — and edges show cursor: pointer without one either.

The exported PNG has a mangled filename

The name embeds DD/MM/YYYY, and browsers rewrite path separators in the download attribute.


Known issues & gotchas

Verified against the code — fix before relying on these paths
IssueWhereImpact
1000 arrow nodes per animated edgeAnimatedArrow loops to 1000, one <path> + <animateMotion> each; the last begins after ~25 minHeavy DOM/CPU cost on graphs with many consumption edges. Its minDuration/maxDuration props are dead
A real 0 renders as --ConsumptionNode uses value || '--'; the tank uses Formatter.defaultValue, also a falsy checkA genuine zero reading is indistinguishable from no data
Fetch errors are invisibleThe data source catches, logs, and returns undefinedUsers see the "contact us" overlay instead of an error — indistinguishable from an unconfigured site
Slashes in the export filenameThe name embeds DD/MM/YYYY or MMM/YYYYBrowsers mangle path separators in the download attribute
Export reads the session unguardedJSON.parse(LocalDBInstance.getItem(loginResponse)).username with no null checkThrows if the session is missing
setState during another component's renderSummaryDetailNode's popup calls setIsBlink in its render bodyReact logs "cannot update a component while rendering a different component"
Empty-dependency useMemosThe legend and the page dropdown both memoise with []Correct only because the key on ReactFlow remounts them. The 5-min refresh does not refresh the page dropdown's option list
Edges look clickable but aren'tAll edges set cursor: pointer and interactionWidth: 100 with no handlerA misleading 100 px-wide hit area
DownloadPopupTable.jsx is deadNever imported, and would throw — it uses useContext and the context without importing eitherDo not revive as-is
Unreachable legend groupInfoPopupDetails.SUMMARY can never match, since infoMenuKey is only ever SUMMARY_INFO or DETAIL_INFODead code
Redundant togglehandleBasicInfoClick calls setOpenInfoPopup twice with the same computed valueHarmless — it toggles once — but confusing
Unused classThe page's .download-image class is never targeted; capture uses the dialog refDead styling hook

Code reference

FileRole
WaterBalancePage.jsxPage shell, provider, empty-state wiring
dataSource/waterBalance.jsThe waterBalance/data call
controller/waterBalanceContoller.jsPass-through to the data source
store/WaterBalanceStore.jsContext state, refetch + auto-refresh logic
enums/waterBalanceEnums.jsView and granularity enums
components/WaterBalanceHeading.jsxTitle, search, granularity select, date picker, Today
components/BuildWaterBalanceGraph.jsxReact Flow host, view toggle, page dropdown, controls
components/WaterBalanceGraphComponents.jsxnodeTypes / edgeTypes / download-edge maps
components/nodes/*CARD, TANK, OVAL, PROCESS, PUMP, VALVE
components/summaryNodes/*The four summary tile types + the member-unit popup
components/edges/*The three live edge types + the still one used for export
components/animated/AnimatedArrow.jsxFlow-direction arrows
components/WaterBalanceDownloadPopup.jsxExport dialog + PNG capture
components/WaterBalanceInfoPopup.jsxLegend
components/ContactForDiagram.jsxNo-diagram fallback
style/markerStyle.jsThe invisible-handle style