Water quality
water_samples is a ledger of samples, each with its field and laboratory results (water samples). The file records only what was measured. Exceedances, ion balance and facies are derived on demand, and drinking water limits are chosen by the reader, never stored (limit sets).
The outputs below come from two example wells: extremoz-rn-barreiras-raso (nitrate rising in a shallow well) and quixeramobim-ce-cristalino-salobro (brackish water in crystalline rock).
Which samples count
A corrected laboratory report is a new sample whose corrects points to the old one, and a result whose validation.status is rejected is excluded from every derivation (result rules). Start from the effective list:
import { getEffectiveWaterSamples, isResultUsable } from '@welldot/utils';
const samples = getEffectiveWaterSamples(well); // not retracted, sorted by datetime, sequence, file order
const latest = samples.at(-1)!;
const usable = latest.results.filter(isResultUsable);
Latest value of a parameter
import { getLatestResult } from '@welldot/utils';
getLatestResult(well, 'nitrate_as_n');
// { sample: { id: 'am-2025', … }, result: { parameter: {…}, value: 12.6, measured_in: 'lab' } }
It skips blanks and rejected results, and matches CAS numbers to their welldot code.
Exceedances
Pick a limit set from @welldot/core (Water quality vocabulary) and compare:
import { BR_GM_MS_888_2021, WHO_GDWQ_2022 } from '@welldot/core';
import { getExceedances } from '@welldot/utils';
getExceedances(latest, BR_GM_MS_888_2021);
// [
// { result_index: 0, parameter: { code: 'ph', … }, limit: { min: 6, max: 9, … }, kind: 'below_min' },
// { result_index: 3, parameter: { code: 'nitrate_as_n', … }, limit: { max: 10, … }, kind: 'above_max' }
// ]
getExceedances(latest, WHO_GDWQ_2022); // [] (WHO limits nitrate as NO₃, not as N)
Each exceedance has the result's index, the parameter, the limit and its kind (above_max, below_min, or a presence where absence is required). Censored values are handled: < 5 cannot exceed a limit of 10. Turbidity in FAU is never compared with nephelometric limits. You can also pass your own Limit[].
Hydrochemistry
import {
getEffectiveWaterSamples,
getHydrochemicalFacies,
getIonBalance,
getPiperCoordinates,
getStiffValues,
} from '@welldot/utils';
const sample = getEffectiveWaterSamples(brackishWell).at(-1)!;
getIonBalance(sample);
// { cations_meq: 44.29, anions_meq: 44.51, error_pct: -0.26 }
getHydrochemicalFacies(sample); // { cation: 'mixed', anion: 'chloride' }
getStiffValues(sample); // meq/L of Na+K, Ca, Mg, Cl, HCO₃+CO₃, SO₄
getPiperCoordinates(sample); // meq % in each triangle and the diamond point
The ion balance error is the usual check on a complete major-ion analysis: within about ±5 % it is considered consistent. Molar masses and charges come from the parameter vocabulary in @welldot/core. getAcidDrainageIndicators(sample) returns net alkalinity and the sulfate/chloride ratio when the analysis has them.
Field QA/QC
| Function | Checks |
|---|---|
getRelativePercentDifferences(well, sampleId) | Relative percent difference per parameter between a field duplicate or split and its parent_sample_id. |
getBlankContamination(well) | Substances detected in field, trip and equipment blanks, with their campaign. |
getHoldingTimes(sample) | Hours from collection to analysis per result, honoring day-resolution dates. |
getReceivedTemperatureCompliance(sample, maxC?) | Whether the sample arrived at the lab at or below maxC (6 °C by default). |
getPurgeStabilization(purge, criteria?, window?) | Whether the purge readings stabilized, per parameter, over the last readings. |
getSampleDepth(well, sample) / isFormationWater(well, sample) | The sampling depth, and whether it lies in a screened interval below the static level. |
import { getRelativePercentDifferences } from '@welldot/utils';
getRelativePercentDifferences(well, 'am-2025-dup');
// [{ key: 'nitrate_as_n', original: 2.8, duplicate: 2.7, rpd_pct: 3.64 }, …]
Warnings
getWaterSampleWarnings(well) lists the water quality conditions the spec marks as warnings, such as a field duplicate without parent_sample_id, a sampling depth outside the screens, a result analyzed before it was collected or a duplicated result, with the sample ids and result indexes (validation warnings).
Display
import { formatWaterQualityResult, parameterUnitSymbol } from '@welldot/utils';
const labels = {
notDetected: 'n.d.',
present: 'present',
absent: 'absent',
estimated: 'est.',
};
for (const r of latest.results) {
console.log(formatWaterQualityResult(r, labels), parameterUnitSymbol(r));
// '< 5' 'mg/L', '0.004 (est.)' 'mg/L', 'absent' '', …
}
Labels are passed in, so you keep your own translations.