Guides

Logger import

Turn a pressure transducer or data logger export into logger_series readings, with column mapping, time zones, merging and downsampling, using @welldot/utils.

A pressure transducer left in a well records the water level every few minutes for months. Since 2.4, such a record is a logger_series hydrodynamic event: a continuous series, not a pumping test and not a static level (logger_series). Each reading has a datetime (an RFC 3339 instant) and a depth below ground.

@welldot/utils turns a logger's CSV export into those readings. There are no vendor-specific parsers: you map the columns once for your logger model.

From CSV to readings

import { buildLoggerReadings, parseDelimitedText } from '@welldot/utils';

const text = `date;time;level
01/03/2025;00:00;12.40
01/03/2025;01:00;12.38
01/03/2025;02:00;---`;

const { rows } = parseDelimitedText(text); // detects , ; or tab, handles quotes

const { readings, skipped } = buildLoggerReadings(rows, {
  headerRows: 1,
  dateColumn: 0,
  timeColumn: 1,
  valueColumn: 2,
  valueKind: 'water_column', // the logger reports water above the sensor
  sensorDepth: 42, // so depth = 42 − value
  dateFormat: 'dmy',
  timeZone: 'America/Belem', // or offset: '-03:00'
});

// readings: [
//   { datetime: '2025-03-01T00:00:00-03:00', depth: 29.6 },
//   { datetime: '2025-03-01T01:00:00-03:00', depth: 29.62 }
// ]
// skipped: [{ row: 3, issue: 'invalid_value' }]
  • valueKind: 'depth' takes the value as the depth to water; 'water_column' converts the height of water above the sensor using sensorDepth.
  • Times are local to the logger. Give the IANA timeZone of the site, and each reading gets the offset in force at that moment, daylight saving included. A fixed offset also works.
  • Rows that cannot be read are listed in skipped with a reason (invalid_date, invalid_value, missing_sensor_depth, missing_offset), never silently dropped.

Into the file

import { inferLoggerInterval } from '@welldot/utils';

well.hydrodynamic_events = [
  ...(well.hydrodynamic_events ?? []),
  {
    id: crypto.randomUUID(),
    type: 'logger_series',
    datetime: readings[0].datetime,
    measurement_method: 'pressure_transducer',
    sensor_depth: 42,
    interval: inferLoggerInterval(readings), // 60 (minutes)
    barometric_compensation: 'barologger', // closed: barologger, vented or none
    aggregation: 'none', // closed: none, hourly or daily
    readings,
  },
];

Check the spec for the meaning of each field; hourly or daily aggregation requires depth_min and depth_max on every reading (logger_series).

Adding a new download

Loggers are downloaded again and again, and the exports overlap:

import { mergeLoggerReadings } from '@welldot/utils';

const { readings: merged, replaced } = mergeLoggerReadings(
  event.readings,
  newReadings,
  'append', // or 'replace'
);

Readings are merged by instant, the incoming value wins on a tie, and the result is sorted. replaced counts the readings that were overwritten (append) or dropped (replace).

Plotting

A year of 15-minute readings is 35,000 points. downsampleLoggerReadings(readings, maxPoints) keeps the first and last readings and the minimum and maximum depth of each bucket, so peaks survive:

import { downsampleLoggerReadings } from '@welldot/utils';

const plotted = downsampleLoggerReadings(event.readings, 2000);

localDateTimeToInstant('2025-03-01T00:00', 'America/Belem') converts one local time to an instant ('2025-03-01T00:00:00-03:00') if you parse files yourself.

Warnings

getOperationWarnings reports logger_reading_below_sensor (a reading deeper than the sensor) and analysis_uses_logger_series (an aquifer analysis citing a logger series, which is not a controlled test) (Operation and production).