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_neutrality → WaterNeutrality (libs/waterNeutrality/src/WaterNeutrality.jsx) |
| Permission | WATER_NEUTRALITY gates the sidebar entry; the route itself is not wrapped |
| Library | libs/waterNeutrality/ — standard dataSource → controller → store → components |
| Endpoint | GET neutrality/ — the only one |
| Params | date1, date2 (DD/MM/YYYY) |
| Default range | Start of the current month → today |
| Max range | 365 days |
| Refresh | None — fetches on mount and on any range change |
| Chart | chart.js + react-chartjs-2 + chartjs-chart-treemap — the only chart.js use in the whole app |
| Target | 100% |
| Display cap | 120% |
WaterNeutralityTreeGraphComponentis 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.
| Status | Effective range | Popup table says |
|---|---|---|
| Water Positive | ≥ 100 | 100 - 120 |
| Water Neutral | 90 – 100 | 90-100 |
| Nearing Neutrality | 75 – 90 | 75-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
| Area | Component | What it shows | On click |
|---|---|---|---|
| Date header | WaterNeutralityDateHeader | A range picker (AppDatePickerSelection, max 365 days), the current Status, and a "See How?" link | "See How?" opens the concept popup |
| Balance figure | WaterTotalValue | Water Neutrality : <value> with (Target : 100%) | — |
| Legend | WaterNeutralityLegends | Water Credited (green) · Water Debited (blue) · a two-arrow "Deviation from set target" key | — |
| Treemaps | WaterNeutralityTreeGraphComponent ×2 | One card each for Total Water Augmented and Total Water Consumed | Clicking a card expands it full-width and hides the other; clicking again restores both |
| Treemap render | TreemapChart | The tiles themselves | Tooltip lists the per-unit rows |
| Summary | WaterNeutralitySummary | Optional bulleted insights — hidden when empty | — |
| Concept popup | WaterNeutralityPopup | Static explanation, the status table, the formula | Close button or backdrop |
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:
| Field | Purpose |
|---|---|
normalizedValue | The tile's area |
value | Compared against target to pick the deviation arrow |
displayValue | The formatted number shown on the tile |
displayName | The sub-category name |
target · displayTarget | Optional; when present adds a (Target : …) line |
units[] | { displayName, displayValue } rows listed in the tooltip |
The label is built as up to four lines:
- A deviation arrow, only when a
targetexists —↓when credited falls below target,↑when debited rises above it, otherwise blank displayValuedisplayName(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
| Endpoint | GET neutrality/ (Urls.getNeutralityData) |
| Params | date1, date2 — both DD/MM/YYYY |
| Fetched on | Mount and any range change — the store only fires when both dates are set |
| Post-processing | The controller derives neutralityStatus and caps waterNeutrality at 120 |
| Analytics | PAGE_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
| State | What the user sees |
|---|---|
| Loading | A centred loader |
| No data at all | A "Data Not Found" panel with a button through to Water Monitoring |
| Range with no data | A per-treemap "Data Not Found" block replaces the tiles |
| Uncomputable percentage | The status reads "Unable to calculate" |
| One treemap expanded | The other is hidden until you click again |
| No summary | The summary block is omitted entirely |
| API failure | ⚠️ The loader hangs forever — see below |
Visibility & access
Which apps register the route
/water_neutrality → WaterNeutrality. 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.
Sidebar placement
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
| Import | Used for |
|---|---|
apiClient · Urls | The single neutrality/ call |
DateFormatter | getStartDate + formatter for the default month-to-date range |
AnalyticsService · AnalyticEvents | PAGE_VIEW on mount |
assets | The 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
| Issue | Where | Impact |
|---|---|---|
| Perpetual loader on any API failure | The 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 runs | One 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 range | neutralityStatusRange declares "Nearing Neutrality" as 75–120, but the earlier "Water Positive" entry is 100 to Infinity and .find() takes the first match | Everything ≥ 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 value | The status is derived before the cap is applied | Consistent today, but a change to either step could desync the label from the number beside it |
| Wrong error message | The data source logs 'Error Fetching Dashboard Data' | Misleading when debugging — this is the neutrality call, not the dashboard |
| Static popup content | The concept text, status table, and formula in WaterNeutralityPopup are hardcoded | They will drift from neutralityStatusRange whenever the bands change |
Code reference
| File | Role |
|---|---|
WaterNeutrality.jsx | Page shell + provider |
dataSource/neutrality.js | The neutrality/ call |
controller/neutralityController.js | Status derivation + the 120 cap |
store/WaterNeutralityStore.js | Context state, default range |
helper/neutralityHelperInstance.js | Status bands, treemap config, legends, label + tooltip formatters |
components/BuildWaterNeutralityDetails.jsx | Layout, date header, expand logic, empty states |
components/WaterNeutralityTreeGraphComponent.jsx | Per-treemap card |
components/TreemapChart.jsx | The chart.js treemap itself |
components/WaterNeutralitySummary.jsx | Optional insights |
components/WaterNeutralityPopup.jsx | The "See How?" explainer |
Related
- Water Balance · Water Usage Ratio · True Cost of Water — sibling efficiency features
- Components → Charts — why this is the one chart.js exception
- Application Routes · Permissions
- API Call Flow · State Management