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 (monitoring → ground_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 unitsborewellGraphData- Historical graph data (3M/6M/1Y)levelGraphData- Today's hourly graph dataparams- 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 pixelsmaxWidth- Tank width in pixelstext- Current level readinggroundText- "Ground Level" labelwaterText- "Water Level" labelpercentage- 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 thepastNumberOfMonthsAPI param for every non-Today period. Thetodayperiod 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
| Scenario | Behavior | Source |
|---|---|---|
| Loading / first render | Until monitoringStore.monitoringScreenData[categoryId] exists, the provider renders a centered <CustomLoader /> in an <Expanded> (90vh) instead of the page. | GroundWaterLevelDataProvider.jsx |
| No service match | init() 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 borewell | The 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 depth | Tank 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 data | GraphMenu passes `groundWaterStore?.borewellGraphData | |
| Depth < 200 ft | calculateCapacity clamps the tank's maxCapacity to a minimum of 200. | GroundWaterLevelPage.jsx |
| Period switch | Changing 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 mount | On 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 error | Neither 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 undermonitoringScreenData[categoryId]['${type}_${date1}'].MonitoringController—getCategoryData(),getGroundWaterGraphData(),getUnitGranularData()(libs/monitoring/src/controller/monitoringController.js), backed byMonitoringDataSource→apiClient+Urls.AppStoreContext—loginData.servicesfor the category lookup.AnalyticsService+AnalyticEvents.PAGE_VIEW(GroundWaterLevelDataProvider.jsx).constants.refreshDuration(libs/shared/src/constants/constants.js).momentfor date formatting.
Shared enums: GroundWaterGraphType, GraphTypeIntValue, GraphTypeDisplayName (libs/shared/src/enums/graphType.js).
Component libraries (libs/components):
GroundWaterTankfrom@aquagen-mf-webapp/components/waterTank— the inverted-fill tank visualization.GroundWaterGraph(bar) andGroundWaterLineGraph(line) from@aquagen-mf-webapp/components/barGraph.FixedBar,SubPageWrapper(helper),OnlineView,SearchComponent,CustomLoader,Expanded, and the logicalIf/IfNothelpers.- MUI (
Box,Button,Container,Divider,Grid,Typography) andlodash.
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_MONITORINGis a no-op guard (isPermitted()returnstruefor it unconditionally —PermissionController.js). - Effective data gate: the user must have a service in
loginData.serviceswithcategoryId === '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);
}, []);
Related Documentation
- Water Flow Monitoring - Flow consumption tracking
- Water Quality Monitoring - Quality parameter monitoring
- Water Stock Levels - Storage tank monitoring
- API & Services - API integration
- Components - GroundWaterTank component reference
- Enums & Configuration - Graph type enums
Last Updated: February 2026
Module Location: libs/monitoring/src/pages/groundWaterLevel/