Skip to main content

Water Neutrality

A sustainability dashboard that measures how much water an organisation credits (augments, replenishes, offsets) against how much it debits (consumes), and expresses the balance as a single Water Neutrality %.

At a glance

Route/water_neutralityWaterNeutrality (libs/waterNeutrality/src/WaterNeutrality.jsx)
PermissionWATER_NEUTRALITY gates the sidebar entry; the route itself is not wrapped
Librarylibs/waterNeutrality/ — standard dataSource → controller → store → components
EndpointGET neutrality/ — the only one
Paramsdate1, date2 (DD/MM/YYYY)
Default rangeStart of the current month → today
Max range365 days
RefreshNone — fetches on mount and on any range change
Chartchart.js + react-chartjs-2 + chartjs-chart-treemap — the only chart.js use in the whole app
Target100%
Display cap120%
What Water Neutrality does NOT do
  • WaterNeutralityTreeGraphComponent is not a tree or node-link graph — it wraps a treemap. There is no React Flow or D3 here.
  • It does not compute the percentage. The API supplies waterNeutrality; the frontend only caps it at 120 and picks a label.
  • It does not poll. Data is fetched on mount and on range changes only.
  • It does not surface API failures. A failed request produces a permanent loader, never the "Data Not Found" panel.
  • The explainer popup is not data-driven — its bands and formula are hardcoded.

The idea

Summary: The page fetches credited and debited water for a date range, draws each as a treemap broken down by sub-category, and reduces the pair to one percentage against a target of 100%.

The formula shown in the "See How?" popup is Water Augmented / Water Consumed x 100.


Status bands

The percentage is resolved by scanning an ordered list and taking the first match:

Summary: Because the list is scanned in order and the first entry is "100 or more with no upper bound", everything at or above 100 is Water Positive — the declared upper bound on "Nearing Neutrality" never takes effect.

StatusEffective rangePopup table says
Water Positive≥ 100100 - 120
Water Neutral90 – 10090-100
Nearing Neutrality75 – 9075-90
Critically Low< 75<75

The status is computed from the raw value, then the value itself is capped at 120 for display — so a 150% result reads "Water Positive, 120".


What's on the page

AreaComponentWhat it showsOn click
Date headerWaterNeutralityDateHeaderA range picker (AppDatePickerSelection, max 365 days), the current Status, and a "See How?" link"See How?" opens the concept popup
Balance figureWaterTotalValueWater Neutrality : <value> with (Target : 100%)
LegendWaterNeutralityLegendsWater Credited (green) · Water Debited (blue) · a two-arrow "Deviation from set target" key
TreemapsWaterNeutralityTreeGraphComponent ×2One card each for Total Water Augmented and Total Water ConsumedClicking a card expands it full-width and hides the other; clicking again restores both
Treemap renderTreemapChartThe tiles themselvesTooltip lists the per-unit rows
SummaryWaterNeutralitySummaryOptional bulleted insights — hidden when empty
Concept popupWaterNeutralityPopupStatic explanation, the status table, the formulaClose button or backdrop
The name is a misnomer

WaterNeutralityTreeGraphComponent is not a node-link tree — it wraps a treemap. There is no React Flow or D3 anywhere in this feature.


The treemap

Registered pieces: LinearScale and Tooltip from chart.js, plus TreemapController and TreemapElement from chartjs-chart-treemap, rendered through react-chartjs-2's generic Chart with type: 'treemap'.

Tiles are sized by normalizedValue, not the raw value.

Per-tile data contract and label composition

Each tile in the tree array is expected to carry:

FieldPurpose
normalizedValueThe tile's area
valueCompared against target to pick the deviation arrow
displayValueThe formatted number shown on the tile
displayNameThe sub-category name
target · displayTargetOptional; when present adds a (Target : …) line
units[]{ displayName, displayValue } rows listed in the tooltip

The label is built as up to four lines:

  1. A deviation arrow, only when a target exists — when credited falls below target, when debited rises above it, otherwise blank
  2. displayValue
  3. displayName
  4. (Target : displayTarget) when a target exists

Both directions of "bad" therefore render in red via the legend's two-arrow key: under-crediting and over-consuming.


Data & API

EndpointGET neutrality/ (Urls.getNeutralityData)
Paramsdate1, date2 — both DD/MM/YYYY
Fetched onMount and any range change — the store only fires when both dates are set
Post-processingThe controller derives neutralityStatus and caps waterNeutrality at 120
AnalyticsPAGE_VIEW on mount
Response shape
{
"waterNeutrality": 0, // the percentage, uncapped
"ID_WATER_CREDITED": {
"total": 0,
"displayTotal": "0",
"subCategory": [ /* treemap tiles, see the contract above */ ]
},
"ID_WATER_DEBITED": {
"total": 0,
"displayTotal": "0",
"subCategory": [ /* ... */ ]
},
"summary": [ /* optional bullet strings */ ]
}

The controller adds two fields on top: the capped waterNeutrality and a neutralityStatus string. The two branch keys come from NeutralityHelperInstance.NeutralityDataEnum, and their display names and colours from treemapConfig — "Total Water Augmented : " in green, "Total Water Consumed : " in blue.

State

WaterNeutralityStore is a React Context (WaterNeutralityProvider / WaterNeutralityContext) exposing neutralityData, params / setParams, and isLoading. See State Management and API Call Flow.


Render states

StateWhat the user sees
LoadingA centred loader
No data at allA "Data Not Found" panel with a button through to Water Monitoring
Range with no dataA per-treemap "Data Not Found" block replaces the tiles
Uncomputable percentageThe status reads "Unable to calculate"
One treemap expandedThe other is hidden until you click again
No summaryThe summary block is omitted entirely
API failure⚠️ The loader hangs forever — see below

Visibility & access

Which apps register the route

/water_neutralityWaterNeutrality. The nav route constant is also aliased as the top-level EFFICIENCY entry, so the Efficiency menu and Water Neutrality resolve to the same path.

Route-level gating: none

The route element is unwrapped. WATER_NEUTRALITY gates the sidebar entry only.

The item sits inside the Efficiency submenu, alongside Water Usage Ratio, True Cost, Labs, and the SCADA entries. Subscription expiry is handled by the global overlay described in Permissions.


Dependencies

Shared store & services

ImportUsed for
apiClient · UrlsThe single neutrality/ call
DateFormattergetStartDate + formatter for the default month-to-date range
AnalyticsService · AnalyticEventsPAGE_VIEW on mount
assetsThe credited/debited palette

Component library

AppDatePickerSelection (range mode, capped at 365 days), plus MUI primitives for the cards, dialog, and table in the explainer popup.

Lib-internal

WaterNeutralityProvider / WaterNeutralityContext, NeutralityController, NeutralityDataSource, and NeutralityHelperInstance — a singleton holding the status bands, treemap config, legends, and the chart label/tooltip formatters.

External npm

chart.js + react-chartjs-2 + chartjs-chart-treemap (registering LinearScale, Tooltip, TreemapController, TreemapElement), @mui/material, moment. This is the only chart.js usage in the application — everything else is Recharts.


Usage examples

Read the neutrality figure and status

import { useContext } from 'react';
import { WaterNeutralityContext } from '@aquagen-mf-webapp/waterNeutrality/store/WaterNeutralityStore';

function NeutralityBadge() {
const { neutralityData, isLoading } = useContext(WaterNeutralityContext);
if (isLoading || !neutralityData) return null;
// waterNeutrality is already capped at 120 by the controller
return (
<span>
{neutralityData.waterNeutrality}%{neutralityData.neutralityStatus}
</span>
);
}

Change the date range

const { params, setParams } = useContext(WaterNeutralityContext);

// the store only fetches when BOTH dates are present
setParams({ date1: '01/07/2026', date2: '30/07/2026' });

Reach the credited and debited branches

import { NeutralityHelperInstance } from '@aquagen-mf-webapp/waterNeutrality/helper/neutralityHelperInstance';

const { ID_WATER_CREDITED, ID_WATER_DEBITED } =
NeutralityHelperInstance.NeutralityDataEnum;

const credited = neutralityData[ID_WATER_CREDITED]; // { total, displayTotal, subCategory }
const tiles = credited.subCategory; // sized by normalizedValue

Fire the page-view event

AnalyticsService.sendEvent(AnalyticEvents.PAGE_VIEW, {}, true);

Troubleshooting

The loader never stops

The most likely failure here. The data source swallows the error and returns undefined; the controller then dereferences response.waterNeutrality and throws; init() has no try/catch, so setIsLoading(false) never runs. Look for Error Fetching Dashboard Data in the console — the message is mislabelled but it is this call.

"Data Not Found" is never shown even though the API is failing

That guard only covers a successful response with no data. A failed request takes the perpetual-loader path above instead.

A result above 100% is labelled Water Positive, not Nearing Neutrality

Correct behaviour. The status list is scanned in order and "100 or more" is first, so the declared 75–120 upper bound on Nearing Neutrality is unreachable.

The number reads 120 but the API returned more

The controller caps the displayed value at 120. The status is derived from the uncapped value first.

The status label disagrees with the popup's table

The popup's concept text, band table, and formula are hardcoded and drift from neutralityStatusRange whenever the bands change.

One treemap disappeared

Clicking a card expands it full-width and hides the other. Click again to restore both.

A tile has no deviation arrow

Arrows only render when the tile carries a target. Credited tiles show a down arrow below target; debited tiles show an up arrow above it.


Known issues & gotchas

Verified against the code
IssueWhereImpact
Perpetual loader on any API failureThe data source catches the error and returns undefined; the controller then immediately reads response.waterNeutrality, which throws. The store's init() has no try/catch, so setIsLoading(false) never runsOne failed request leaves the page spinning forever. The "Data Not Found" guard is never reached, because it only handles a successful empty response
A dead status rangeneutralityStatusRange declares "Nearing Neutrality" as 75–120, but the earlier "Water Positive" entry is 100 to Infinity and .find() takes the first matchEverything ≥ 100 is Water Positive; the 120 bound is unreachable. The popup's 100 - 120 wording only looks right because the display value is separately capped at 120
Status uses the uncapped valueThe status is derived before the cap is appliedConsistent today, but a change to either step could desync the label from the number beside it
Wrong error messageThe data source logs 'Error Fetching Dashboard Data'Misleading when debugging — this is the neutrality call, not the dashboard
Static popup contentThe concept text, status table, and formula in WaterNeutralityPopup are hardcodedThey will drift from neutralityStatusRange whenever the bands change

Code reference

FileRole
WaterNeutrality.jsxPage shell + provider
dataSource/neutrality.jsThe neutrality/ call
controller/neutralityController.jsStatus derivation + the 120 cap
store/WaterNeutralityStore.jsContext state, default range
helper/neutralityHelperInstance.jsStatus bands, treemap config, legends, label + tooltip formatters
components/BuildWaterNeutralityDetails.jsxLayout, date header, expand logic, empty states
components/WaterNeutralityTreeGraphComponent.jsxPer-treemap card
components/TreemapChart.jsxThe chart.js treemap itself
components/WaterNeutralitySummary.jsxOptional insights
components/WaterNeutralityPopup.jsxThe "See How?" explainer