Skip to main content

Ground Water Level Monitoring

Monitor ground water levels and borewell depths with animated tank visualization, multi-period trend graphs (Today, 3 Months, 6 Months, 1 Year), and historical analysis.


Overview

The Ground Water Level Monitoring feature tracks ground water table depths and borewell water levels with visual tank representations, switchable time-period graphs, and comparative analysis across multiple time ranges.

Location: libs/monitoring/src/pages/groundWaterLevel/GroundWaterLevelPage.jsx

Route: /monitoring/ground_water_level/:categoryId

The route is a nested child of monitoring (monitoringground_water_level/:categoryId), not a flat /monitoring/groundwater/... path. Unlike Water Quality, it is registered in only two apps: apps/production/src/routes/routes.js and apps/demo/src/routes/routes.js. It is not registered in rwi, uwms, or lakepulse.

Visibility / Access Control: The page has no route-level PermissionWrapper, and WATER_MONITORING does not gate it — PermissionController.isPermitted() returns true for WATER_MONITORING unconditionally (libs/shared/src/controller/permission/PermissionController.js). Access is gated by (1) whether the app registers the route (only production and demo); (2) whether loginData.services contains a service with categoryId === 'GROUND_WATER_LEVEL' (looked up at GroundWaterLevelPage.jsx and GroundWaterLevelDataProvider.jsx); and (3) the sidebar lock state. See Visibility Parameters.


Key Features

1. Ground Water Tank Visualization

Specialized ground water tank display showing:

Tank Components:

  • Ground Level - Surface reference line (0 ft)
  • Water Level - Current depth below ground (e.g., 32 ft)
  • Inverted Fill - Fills from bottom as water level rises
  • Numeric Reading - Current depth with SI unit
  • Animated Wave - Realistic water wave at measured depth

Visual Logic:

  • Deeper levels = Lower water in tank visualization
  • Shallower levels = Higher water in tank visualization
  • Percentage calculation: 100 - ((currentLevel - 32) / 7)

2. Multi-Period Graph Analysis

Switchable graph views with a button toggle. There are five periods (from GroundWaterGraphType, libs/shared/src/enums/graphType.js), rendered in this order (GroundWaterLevelPage.jsx):

Available Periods:

  • Today (today) - Hourly line graph for the current day
  • This Month (thisMonth)
  • Past 3 Months (past3Months)
  • Past 6 Months (past6Months)
  • This Year (thisYear)

Graph Types:

  • Line Graph (GroundWaterLineGraph) - Only for the Today view (hourly granular data)
  • Bar Graph (GroundWaterGraph) - For every non-Today period (GroundWaterLevelPage.jsx)

3. Real-Time Updates

Auto-Refresh:

  • Page auto-refreshes at configured intervals
  • Loads today's data for all units on mount
  • Fetches granular data based on selected period

4. Online/Offline Status

Each borewell displays:

  • Online: Green "Online" badge
  • Offline: "Last Update" timestamp with reduced opacity

Architecture

Data Flow

Key Components

1. GroundWaterLevelScreenDataProvider

  • Purpose: State management for ground water monitoring
  • Location: libs/monitoring/src/dataProvider/GroundWaterLevelDataProvider.jsx
  • State Variables:
    • groundWaterData - Current water levels for all units
    • borewellGraphData - Historical graph data (3M/6M/1Y)
    • levelGraphData - Today's hourly graph data
    • params - Query parameters (date, category, type)

Context Value:

{
groundWaterData: {
data: {
subCategories: [
{
id: 'SUB_CAT_1',
displayName: 'Borewells',
units: [
{
unitId: 'UNIT_1',
displayName: 'Borewell 1',
value1: 35.2, // Current depth (ft)
online: true,
lastUpdatedAt: '25/02/2026 10:30 AM'
}
]
}
]
}
},
borewellGraphData: [
{ x: 'Nov', y: 33.5 },
{ x: 'Dec', y: 34.2 },
{ x: 'Jan', y: 35.0 }
],
levelGraphData: [
{ x: '00:00', y: 35.0 },
{ x: '01:00', y: 35.1 },
// ... hourly data for today
],
setParams: (params) => {},
getGroundWaterGraphData: (startDate, unitId, pastNumberOfMonths) => {},
getGranularUnitData: (unitId, date1) => {}
}

2. GroundWaterLevelPage

  • Purpose: Main ground water monitoring page
  • Location: libs/monitoring/src/pages/groundWaterLevel/GroundWaterLevelPage.jsx
  • Features:
    • Fixed header with title
    • Displays all subcategories
    • Renders unit tanks with graphs
    • Manages graph period selection

3. BuildUnitsView

  • Purpose: Render individual borewell units
  • Features:
    • Two-column layout (tank | graph)
    • Unit name and online status
    • Ground water tank visualization
    • Switchable period graph

4. GraphMenu

  • Purpose: Period selection buttons and graph display
  • Features:
    • Button toggle for Today/3M/6M/1Y
    • Conditional graph rendering (line vs bar)
    • Auto-fetch data on period change

5. GroundWaterTank Component

  • Purpose: Specialized tank for ground water visualization
  • Location: libs/components/src/waterTank/GroundWaterTank.jsx
  • Props:
    • maxCapacity - Maximum depth (calculated)
    • maxHeight - Tank height in pixels
    • maxWidth - Tank width in pixels
    • text - Current level reading
    • groundText - "Ground Level" label
    • waterText - "Water Level" label
    • percentage - Fill percentage (inverted)
    • siUnit - Unit label (ft, m)
    • waveColor - Water color (#64B5F6 blue)

6. Graph Components

  • GroundWaterGraph - Bar chart for 3M/6M/1Y data
  • GroundWaterLineGraph - Line chart for Today data
  • Location: libs/components/src/barGraph/

API Integration

All endpoint constants live in libs/shared/src/services/api/urls.js and are appended to the environment base URL (urls.js).

Get Category Data (Current Levels)

Same shared path as every monitoring page: GroundWaterLevelDataProvider.init()MonitoringStore.getCategoryData()MonitoringController.getCategoryData()MonitoringDataSource.getCategoryData()apiClient.get(Urls.categoryData, params). Urls.categoryData resolves to deviceData/ (urls.js).

GET deviceData/
Params: {
date1: '25/02/2026', // DD/MM/YYYY, defaults to today
category: 'GROUND_WATER_LEVEL', // the service categoryId / :categoryId route param
type: 'HOUR',
divisionFactor: 1 // set in the provider params (GroundWaterLevelDataProvider.jsx)
}

Response: {
siUnit: 'ft',
subCategories: [
{
id: 'SUB_CAT_1',
displayName: 'Borewells',
units: [
{
unitId: 'UNIT_1',
displayName: 'Borewell 1',
value1: 35.2,
online: true,
lastUpdatedAt: '25/02/2026 10:30 AM'
},
{
unitId: 'UNIT_2',
displayName: 'Borewell 2',
value1: 42.8,
online: false,
lastUpdatedAt: '24/02/2026 08:15 PM'
}
]
}
]
}

Get Ground Water Graph Data (This Month / Past 3M / Past 6M / This Year)

GraphMenu → context getGroundWaterGraphData()MonitoringController.getGroundWaterGraphData()MonitoringDataSource.getGroundWaterGraphData()apiClient.get(Urls.getGroundWaterGraphData, params). Urls.getGroundWaterGraphData resolves to groundWaterLevel/graph (urls.js). The provider stores response.graphData into borewellGraphData (GroundWaterLevelDataProvider.jsx).

GET groundWaterLevel/graph
Params: {
startDate: '25/02/2026', // moment().format('DD/MM/YYYY')
unitId: 'UNIT_1',
pastNumberOfMonths: 3 // GraphTypeIntValue[selectedMonth]: thisMonth=1, past3Months=3, past6Months=6, thisYear=(current month number)
}

Response: {
graphData: [
{ x: 'Nov', y: 33.5 },
{ x: 'Dec', y: 34.2 },
{ x: 'Jan', y: 35.0 }
]
}

Get Granular Unit Data (Today)

GraphMenu (Today) and the mount effect → context getGranularUnitData()MonitoringController.getUnitGranularData()MonitoringDataSource.getUnitGranularData()apiClient.get(Urls.granularUnitData, params). Urls.granularUnitData resolves to /granular/unit (urls.js). The provider stores response.lineGraph1 into levelGraphData (GroundWaterLevelDataProvider.jsx).

GET /granular/unit
Params: {
date1: '25/02/2026',
unitId: 'UNIT_1',
summation: false
}

Response: {
lineGraph1: [
{ x: '00:00', y: 35.0 },
{ x: '01:00', y: 35.1 },
{ x: '02:00', y: 35.0 },
// ... hourly readings
]
}

Graph Type Enums

Location: libs/shared/src/enums/graphType.js (exported via the shared enums barrel).

import moment from 'moment';

const GroundWaterGraphType = {
today: 'today',
thisMonth: 'thisMonth',
past3Months: 'past3Months',
past6Months: 'past6Months',
thisYear: 'thisYear',
};

const GraphTypeIntValue = {
today: 1,
thisMonth: 1,
past3Months: 3,
past6Months: 6,
thisYear: moment().month() + 1, // current month number (1-12)
};

const GraphTypeDisplayName = {
today: 'Today',
thisMonth: 'This Month',
past3Months: 'Past 3 Months',
past6Months: 'Past 6 Months',
thisYear: 'This Year',
};

Note: GraphTypeIntValue[selectedMonth] is passed as the pastNumberOfMonths API param for every non-Today period. The today period does not use this value — it calls the granular-unit endpoint instead.


Usage Examples

1. Initialize Ground Water Monitoring

import { GroundWaterLevelScreenDataProvider } from '@aquagen-mf-webapp/monitoring/dataProvider/GroundWaterLevelDataProvider';

function GroundWaterApp() {
const { categoryId } = useParams();

return (
<GroundWaterLevelScreenDataProvider categoryId={categoryId}>
<GroundWaterContent />
</GroundWaterLevelScreenDataProvider>
);
}

2. Access Ground Water Data

import { useContext } from 'react';
import { GroundWaterLevelDataContext } from '@aquagen-mf-webapp/monitoring/dataProvider/GroundWaterLevelDataProvider';

function GroundWaterWidget() {
const groundWaterStore = useContext(GroundWaterLevelDataContext);
const { groundWaterData } = groundWaterStore;

return (
<div>
{groundWaterData?.data?.subCategories?.map(category => (
<div key={category.id}>
<h3>{category.displayName}</h3>
{category.units.map(unit => (
<div key={unit.unitId}>
<h4>{unit.displayName}</h4>
<p>Depth: {unit.value1?.toFixed(2)} ft</p>
<p>Status: {unit.online ? 'Online' : 'Offline'}</p>
</div>
))}
</div>
))}
</div>
);
}

3. Switch Graph Period

import { useState, useEffect } from 'react';
import { GroundWaterGraphType, GraphTypeIntValue } from '@aquagen-mf-webapp/shared/enums';

function GraphMenu({ unitId }) {
const groundWaterStore = useContext(GroundWaterLevelDataContext);
const [selectedMonth, setSelectedMonth] = useState(GroundWaterGraphType.today);

useEffect(() => {
const startDate = moment().format('DD/MM/YYYY');

if (selectedMonth === GroundWaterGraphType.today) {
// Fetch hourly data for today
groundWaterStore.getGranularUnitData(unitId, startDate);
} else {
// Fetch monthly data for 3M/6M/1Y
groundWaterStore.getGroundWaterGraphData(
startDate,
unitId,
GraphTypeIntValue[selectedMonth]
);
}
}, [selectedMonth]);

return (
<div>
{/* Period selection buttons */}
{Object.entries(GroundWaterGraphType).map(([key, value]) => (
<button
key={key}
onClick={() => setSelectedMonth(value)}
style={{
borderColor: selectedMonth === value ? '#00374A' : '#CBCBCB',
color: selectedMonth === value ? '#00374A' : 'black'
}}
>
{GraphTypeDisplayName[value]}
</button>
))}

{/* Conditional graph rendering */}
{selectedMonth === GroundWaterGraphType.today ? (
<GroundWaterLineGraph data={groundWaterStore.levelGraphData || []} />
) : (
<GroundWaterGraph
selectedMonth={selectedMonth}
data={groundWaterStore.borewellGraphData || []}
/>
)}
</div>
);
}

4. Calculate Tank Capacity

// Actual implementation (GroundWaterLevelPage.jsx)
const calculateCapacity = (value) => {
if (value < 200) {
return 200; // Minimum capacity
}

// Round up to the nearest 500
return Math.ceil(value / 500) * 500 === value
? Math.ceil(value + 1 / 500) * 500 // NOTE: `1 / 500` binds before the add (operator-precedence quirk in source)
: Math.ceil(value / 500) * 500;
};

// Usage (GroundWaterLevelPage.jsx)
const maxCapacity = calculateCapacity((unit.value1 ?? 0) + 100);

5. Calculate Fill Percentage (Inverted)

// Ground water tanks fill from bottom, deeper = less fill
function calculateGroundWaterPercentage(currentLevel) {
// Assuming ground level is 32 ft and range is 7 ft
const groundLevel = 32;
const range = 7;

// Inverted: 100% at 32 ft (shallowest), 0% at 39 ft (deepest)
const percentage = 100 - ((currentLevel - groundLevel) / range);

return Math.max(0, Math.min(100, percentage));
}

// Usage
const fillPercentage = calculateGroundWaterPercentage(unit.value1 ?? 200);

Ground Water Tank Visualization

<GroundWaterTank
maxCapacity={calculateCapacity((unit.value1 ?? 0) + 100)}
maxHeight={190}
maxWidth={200}
text={`${unit.value1?.toFixed(2) ?? '--'} ${siUnit}`}
groundText="Ground Level"
waterText="Water Level"
percentage={100 - ((unit.value1 ?? 200) - 32) / 7}
siUnit={siUnit}
style={{ borderColor: 'black' }}
waveColor="#64B5F6" // Blue water
/>

Visual Structure:


Graph Rendering Logic

Conditional Graph Display

{/* Show Line Graph for Today */}
<If condition={selectedMonth === GroundWaterGraphType.today}>
<GroundWaterLineGraph
selectedMonth={selectedMonth}
data={groundWaterStore?.levelGraphData || []}
/>
</If>

{/* Show Bar Graph for 3M/6M/1Y */}
<If condition={selectedMonth !== GroundWaterGraphType.today}>
<GroundWaterGraph
selectedMonth={selectedMonth}
data={groundWaterStore?.borewellGraphData || []}
/>
</If>

Period Button Styling

const buttonStyle = {
margin: '2px',
backgroundColor: 'white',
border: '1px solid #CBCBCB',
borderRadius: '2px',
fontSize: '12px',
minWidth: 'auto',
color: 'black',
marginRight: '8px',
textTransform: 'capitalize',

'&.Mui-selected': {
borderColor: '#00374A',
color: '#00374A'
}
};

<Button
sx={{
...buttonStyle,
borderColor: selectedMonth === v ? '#00374A' : '#CBCBCB',
color: selectedMonth === v ? '#00374A' : 'black'
}}
onClick={() => setSelectedMonth(v)}
>
{GraphTypeDisplayName[v]}
</Button>

Auto-Fetch on Mount

// Fetch today's data for all units when component mounts
useEffect(() => {
units.forEach((unit) => {
if (unit?.unitId) {
groundWaterStore.getGranularUnitData(
unit.unitId,
moment(new Date()).format('DD/MM/YYYY')
);
}
});
}, [units]);

Auto-Refresh

The provider loads current levels on mount and re-runs init() on an interval (GroundWaterLevelDataProvider.jsx). constants.refreshDuration is 5 minutes (5 * 60 * 1000 ms — libs/shared/src/constants/constants.js).

useEffect(() => {
init(); // Initial load

const interval = setInterval(() => {
init(); // Background refresh every 5 minutes
}, constants.refreshDuration);

return () => clearInterval(interval);
}, []);

Responsive Layout

Desktop (md+):

  • Two-column grid layout
    • Left: Tank visualization (33% width)
    • Right: Graph area (67% width)
  • Vertical divider between columns

Mobile (xs-sm):

  • Stacked vertical layout
  • Horizontal divider between tank and graph
  • Full-width components

Grid Configuration:

<Grid container columnSpacing={1}>
<Grid item size={{ xs: 12, md: 4 }}>
{/* Tank visualization */}
</Grid>

<Divider
orientation="vertical"
flexItem
sx={{ display: { xs: 'none', md: 'block' } }}
/>

<Grid item size={{ xs: 12, md: 7 }}>
{/* Graph area */}
</Grid>
</Grid>

Edge Cases

ScenarioBehaviorSource
Loading / first renderUntil monitoringStore.monitoringScreenData[categoryId] exists, the provider renders a centered <CustomLoader /> in an <Expanded> (90vh) instead of the page.GroundWaterLevelDataProvider.jsx
No service matchinit() looks up the service by categoryId; if loginData is falsy it uses [] and reads filteredItem['categoryId'] — a missing service means the category lookup returns undefined and no valid data is cached. categoryData on the page is [].GroundWaterLevelDataProvider.jsx, GroundWaterLevelPage.jsx
Offline borewellThe tank column and graph column are dimmed via opacity: unit.online ? 1 : 0.5; the <OnlineView /> badge is replaced by a Last Update: {lastUpdatedAt} line.GroundWaterLevelPage.jsx, 176-183, 219
Null / undefined depthTank text uses unit.value1?.toFixed(2) ?? '--'; the fill percentage falls back to unit.value1 ?? 200 (renders near-empty); calculateCapacity receives (unit.value1 ?? 0) + 100.GroundWaterLevelPage.jsx, 190-195
Missing SI unit`categoryData['siUnit']
Empty graph dataGraphMenu passes `groundWaterStore?.borewellGraphData
Depth < 200 ftcalculateCapacity clamps the tank's maxCapacity to a minimum of 200.GroundWaterLevelPage.jsx
Period switchChanging selectedMonth re-fires the effect: Today → getGranularUnitData; any other → getGroundWaterGraphData with GraphTypeIntValue[selectedMonth]. selectedMonth defaults to today.GroundWaterLevelPage.jsx, 58-71
Per-unit granular on mountOn mount / when units change, BuildUnitsView fetches today's granular data for every unit in the subcategory. Multiple borewells therefore issue multiple /granular/unit calls.GroundWaterLevelPage.jsx
API errorNeither getGroundWaterGraphData nor getGranularUnitData in the provider has a try/catch; a rejected apiClient.get leaves borewellGraphData / levelGraphData unset (charts render empty). init() likewise has no try/catch, so a failed category call keeps the loader visible.GroundWaterLevelDataProvider.jsx
Permission-denied (page)Not applicable at the page level (no PermissionWrapper). Access is blocked only by the route not being registered (rwi/uwms/lakepulse) or by the sidebar lock (see Visibility Parameters).routes.js

Dependencies

Shared store / services:

  • MonitoringStore (MonitoringStoreContext) — getCategoryData() for current levels; caches under monitoringScreenData[categoryId]['${type}_${date1}'].
  • MonitoringControllergetCategoryData(), getGroundWaterGraphData(), getUnitGranularData() (libs/monitoring/src/controller/monitoringController.js), backed by MonitoringDataSourceapiClient + Urls.
  • AppStoreContextloginData.services for the category lookup.
  • AnalyticsService + AnalyticEvents.PAGE_VIEW (GroundWaterLevelDataProvider.jsx).
  • constants.refreshDuration (libs/shared/src/constants/constants.js).
  • moment for date formatting.

Shared enums: GroundWaterGraphType, GraphTypeIntValue, GraphTypeDisplayName (libs/shared/src/enums/graphType.js).

Component libraries (libs/components):

  • GroundWaterTank from @aquagen-mf-webapp/components/waterTank — the inverted-fill tank visualization.
  • GroundWaterGraph (bar) and GroundWaterLineGraph (line) from @aquagen-mf-webapp/components/barGraph.
  • FixedBar, SubPageWrapper (helper), OnlineView, SearchComponent, CustomLoader, Expanded, and the logical If/IfNot helpers.
  • MUI (Box, Button, Container, Divider, Grid, Typography) and lodash.

Note: the ground-water page does not import recharts or PermissionWrapper directly — charts are encapsulated inside the barGraph components.


Visibility Parameters

Apps that register the route: only production (routes.js) and demo (routes.js). rwi, uwms, and lakepulse do not register it, so the page is unreachable there regardless of permissions.

Permission / gating:

  • No route-level PermissionWrapper; WATER_MONITORING is a no-op guard (isPermitted() returns true for it unconditionally — PermissionController.js).
  • Effective data gate: the user must have a service in loginData.services with categoryId === 'GROUND_WATER_LEVEL' (GroundWaterLevelDataProvider.jsx).

Sidebar / nav entry: The sidebar is a static menu tree. The "Ground Water Level" entry is hard-coded under the "Monitoring" (METRICS) parent:

  • Path: GROUND_WATER_PAGE: '/monitoring/ground_water_level/GROUND_WATER_LEVEL' (libs/shared/src/helper/navHelperInstance.js).
  • permissionId: StandardCategoryTypeUppercase.GROUND_WATER_LEVEL ('GROUND_WATER_LEVEL'), displayName: 'Ground Water Level', lockedPath: '/feature_locked/MONITORING/GROUND_WATER' (navHelperInstance.js).

Sidebar lock state: Monitoring items default to SidebarAccess.LOCKED (libs/shared/src/enums/sidebarAccess.js). getSidebarItemPermissionState sets isLocked = !isPermitted(permissionId, loginData) (libs/components/src/permissionWrapper/SidebarPermissionCheck.jsx), so a non-permitted item is shown with a lock icon (not hidden); clicking it routes to lockedPath (/feature_locked/MONITORING/GROUND_WATER) instead of the real page (libs/components/src/appNavBar/components/SideBarMenus.jsx, 421-431). The feature-locked landing is defined at libs/featureLocked/src/appNavHelpers/lockedFeatureHelper.js.

How the item unlocks: GROUND_WATER_LEVEL is not in allPermission (libs/shared/src/enums/permissions.js), so it enters the permission set only via getAllUserPermissions, which pushes each service.categoryId (PermissionController.js). Thus the item unlocks only when the user has a service with categoryId === 'GROUND_WATER_LEVEL', or is a SUPER_USER (which bypasses the check for this non-exception tag — PermissionController.js).

isDemo: isDemoOption: false for this item (navHelperInstance.js), so the demo-only sidebar filter never removes it.

Subscription expiry: Global, not per-item. When the subscription errorState === EXPIRED, SubscriptionOverlay renders a full-screen fixed overlay over the whole app (libs/shared/src/components/subscriptionRenewal/components/SubscriptionOverlay.jsx, computed by libs/shared/src/helper/subscriptionHelper.js). PRE/POST states show dialogs. This blocks the ground-water page without changing the sidebar lock state.


Best Practices

1. Handle Missing Data

// Safe access to nested properties
const currentLevel = unit.value1?.toFixed(2) ?? '--';
const siUnit = categoryData?.siUnit || '';

2. Default to Today View

const [selectedMonth, setSelectedMonth] = useState(GroundWaterGraphType.today);

3. Show Offline Status Clearly

<If condition={unit.online}>
<OnlineView />
</If>

<IfNot condition={unit.online}>
<Typography fontSize={12}>
Last Update: {unit.lastUpdatedAt}
</Typography>
</IfNot>

{/* Reduce opacity for offline units */}
<Box sx={{ opacity: unit.online ? 1 : 0.5 }}>
{/* Tank and graph */}
</Box>

4. Dynamic Capacity Calculation

Always calculate capacity dynamically to accommodate varying water levels:

const maxCapacity = calculateCapacity((unit.value1 ?? 0) + 100);

Troubleshooting

Graph Not Displaying

Cause: Missing graph data or incorrect period selected Solution: Check that API returned data and period button is selected

const graphData = selectedMonth === GroundWaterGraphType.today
? groundWaterStore?.levelGraphData
: groundWaterStore?.borewellGraphData;

if (!graphData || graphData.length === 0) {
return <NoDataMessage />;
}

Tank Percentage Incorrect

Cause: Inverted calculation logic not applied Solution: Use inverted formula for ground water

// Correct (inverted)
percentage = 100 - ((currentLevel - groundLevel) / range);

// Incorrect (normal tank)
percentage = (currentLevel / maxCapacity) * 100;

Period Button Not Updating

Cause: State not triggering API call Solution: Add selectedMonth to useEffect dependencies

useEffect(() => {
// Fetch data
}, [selectedMonth]); // Add dependency

Analytics Integration

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

useEffect(() => {
AnalyticsService.sendEvent(AnalyticEvents.PAGE_VIEW, {}, true);
}, []);


Last Updated: February 2026 Module Location: libs/monitoring/src/pages/groundWaterLevel/