What a CSV of Monitoring Wells Has to Look Like
There is no template to match and no field to fill in first. Drop the file you already have in and every column is given a role, in front of you, with the reason it was given. This page is what those rules are — written for somebody with the file open in the next window, who wants to know before they bother whether the awkward part of it will survive.
The smallest file that draws something
Three columns and two rows: a position and a number. A name column is better and is not required. A date column is only needed by the maps that compare one round with another.
Everything else on this page is about a file that has more than that in it, which yours will.
| Well | Easting | Northing | Head |
|---|---|---|---|
| MW-1 | 100 | 200 | 38.14 |
| MW-2 | 150 | 260 | 37.63 |
| MW-3 | 210 | 215 | 36.95 |
The separator, the decimal mark and the encoding
All three are worked out from the file. There is no separator setting, no encoding menu and no decimal-mark option anywhere in this product, and there is not going to be one: a parser knob is a question asked before the reader has seen anything, and the answer is in the file already.
The separator is chosen by which candidate makes the table rectangular rather than by which one occurs most often — the difference matters on a European export, where a semicolon-separated line can easily hold more commas than semicolons.
| In your file | What happens to it |
|---|---|
| A comma, a tab, a semicolon or a pipe between fields | Chosen by which one makes every row the same width, and named in the report. |
| 0,45 in a semicolon-separated file | Read as forty-five hundredths, because a decimal comma could not have survived a comma-separated file. |
| 512,340 in a comma-separated file | Read as five hundred and twelve thousand — a surviving comma there can only be a thousands separator. |
| 1.234,5 | Read as one thousand two hundred and thirty-four point five: whichever mark comes last is the decimal one. |
| 1.5E-9 | Read as scientific notation. |
| A field wrapped in double quotes, with a comma inside it | Split on the quotes rather than on the comma, and a doubled quote inside becomes one quote. |
| A stray inch mark: MW-3 6" casing | Taken literally. An unbalanced quote is never a reason to refuse a table. |
| A byte-order mark at the start of the file | Stripped, so the first column header is not silently unmatchable. |
| A file saved as UTF-16 rather than UTF-8 | Recognised by its byte-order mark and decoded as UTF-16, rather than arriving as a column of nulls that looks fine in the paste box. |
Headers, units and the row they sit on
Headers go on one row, above the data. A unit in brackets after the name is expected and is kept — it goes on the figure rather than being thrown away — and it is ignored when the header is matched against the roles below, so DTW (ft btoc) and Depth to Water are the same column to this reader and two different labels on the sheet.
A file with no header row at all still reads. The columns get positional names and the report says they were invented, which is the honest version of a guess.
| In your file | What happens to it |
|---|---|
| DTW (ft btoc) | Matched on the name with the bracketed unit set aside, and the unit kept for the figure. |
| A first row that is already data | Recognised as data, not promoted to a header, and the columns are named by position instead. |
| Two columns both called Result | The second becomes Result (2) rather than one quietly replacing the other. |
| A row with one more field than the header has | The header is extended with a positional name and the row is counted as ragged. Losing a column in silence is the worse failure. |
| A row with fewer fields than the header | Padded out with blanks. |
| Blank lines between rows | Dropped, and every row still reports the line number it came from in your own copy of the file. |
What each column is taken to be
Every column gets one role and a reason, and both are shown in a table you can read before you look at the map. A role is one click to change, and changing one re-reads the file rather than patching the result.
The roles that matter: the well name, the coordinate pair, the date, a top-of-casing elevation, a depth to water, a value to map, and a qualifier flag belonging to the value beside it. Anything the reader recognises as bookkeeping — a laboratory name, a method code, a reporting limit, a notes column — is read and marked ignored rather than offered as something to contour.
| In your file | What happens to it |
|---|---|
| DTW, Depth to Water, Static Water Level, SWL | All four are recognised as the depth to water. |
| A column of dates headed something useless, like Column 4 | Found by what is in it rather than by what it is called. |
| A Reporting Limit column | Read and then left alone. A map of detection limits is a map of the laboratory’s price list, not of the site. |
| A bare Qualifier column | Attached to the value column immediately to its left. |
| A column called Benzene Qual, anywhere in the row | Attached to the Benzene column by name, wherever it sits. |
| No column that reads as a well name | The points are numbered — Point 1, Point 2 — and the report says why. |
| A role you disagree with | Overriding it re-parses the file, marks the column as yours, and records that the coordinate frame was your decision rather than an inference. |
Top of casing minus depth to water
A field sheet carries a surveyed top-of-casing elevation and a depth measured down from it. A contour map needs the difference. When both columns are in the file and no groundwater elevation is, that subtraction is done here and the new column is added — with the arithmetic written out in your own header spellings, so the report says what was subtracted from what rather than announcing that a column appeared.
The unit comes off the elevation column and nothing is ever converted. If your elevations are in feet above mean sea level, the computed head is in feet above mean sea level, and it says so on the figure.
When the file already has an elevation column, nothing is added. The subtraction is done anyway and used to CHECK the column that is there, which is how a transcription error in one row gets found before it reaches a contour.
| In your file | What happens to it |
|---|---|
| TOC Elevation (ft AMSL) and Depth to Water (ft), no elevation column | A groundwater elevation column is computed and added. |
| 45.00 minus 6.86 | Becomes 38.14, at full precision, in the elevation column’s own unit. |
| An elevation column in feet above mean sea level | The computed head takes that unit. Nothing is converted, ever. |
| TOC, DTW and a GW Elevation column that all agree | Nothing is added and the report says every row was checked. |
| A row where the file’s own elevation disagrees with the subtraction | A warning naming how many rows disagree. The file’s column is still used. |
- The subtraction is arithmetic and not hydrogeology. It cannot tell you that a depth was measured from ground level rather than from the casing, or that one well’s reference point was re-surveyed last year. Those are still yours to check.
Non-detects and qualifier flags
A result that was never detected is not a zero and it is not missing. It is a statement that the true value is somewhere below a limit, and it stays that way through the parser: the bound is kept, the laboratory’s own notation is kept, and the number of them is stated in the report and on the figure.
The distinction that matters most is between a non-detect WITH a reporting limit and one without. <0.5 says the value is below half a microgram; a bare ND says only that nothing was found, which is a fact about the well and not a number. The second is kept as censored with no value at all rather than being given an invented limit, and it is counted separately so a plume map can say how many of its control points were like that.
Qualifier letters follow the EPA National Functional Guidelines set — U, UJ, J, NJ and R change what a value means, and B and C are recognised so they are not mistaken for part of the number while leaving the value a detection. A letter that is not in that list is kept verbatim and given no meaning at all.
| In your file | What happens to it |
|---|---|
| <0.5 | Left-censored at 0.5: the bound is kept and the value is never written as zero. |
| ≤ 1.2 | Read exactly as < is. |
| ND | Censored with no limit at all — which is a different fact from a bound, and is kept as one. |
| ND<0.5, ND 0.5, U 0.5 | All three are a non-detect bounded at 0.5. |
| 0.42 J | The number stays usable and the estimate flag is kept beside it. |
| 12.0 R | Rejected. The value is dropped from the arithmetic and the well still posts on the map. |
| A U in the qualifier column beside a plain 0.5 | Turns that 0.5 into a non-detect bounded at 0.5. |
| A qualifier letter nobody recognises | Kept verbatim, given no meaning, and the value left as a detection. |
| <0,45 in a semicolon-separated file | The decimal comma is read inside the censored value too. |
| 5 mg | Refused rather than read as 5. A number that has quietly lost its unit is worse than a cell nobody could read. |
| Blank, NA, n/a, NULL, a dash, #DIV/0! | All treated as no measurement, not as an error and not as zero. |
| Dry, NM, not sampled | Field notation for "there is no reading here", and read as exactly that. |
Gauging dates
The expensive failure here is a whole monitoring record shifted by up to eleven months because 03/04/2026 was read the wrong way round. So the day and month order is decided once, for the COLUMN, from the evidence in it: one unambiguous cell anywhere in the column — a 14 in the first position, a 13 in the second — settles every other cell in it.
When nothing in the column can settle it, the report says so rather than picking in silence, and a column containing both readings is flagged as contradicting itself.
Every date is read as UTC midnight, so the same file produces the same map on a laptop in Denver and a laptop in Berlin.
| In your file | What happens to it |
|---|---|
| 2026-03-14 | Read without needing an order to be decided. |
| 14-Mar-2026, Mar 14, 2026, 14 March 2026 | All read, either way round, because a month name is unambiguous. |
| 20260314 | Read as a compact year-month-day. |
| 03/04/2026 in a column that also contains 14/04/2026 | The whole column is settled as day-first by that one cell. |
| A column of nothing but ambiguous dates | Read month-first and the report says the order was a default rather than a reading. |
| A column containing 14/04/2026 and 04/14/2026 | Flagged as containing both readings, because no single order can be right. |
| 2026-03-14 1:30 PM | The time of day is kept. |
| 2026-02-31 | Refused, rather than rolled forward into March. |
| 3/14/98 | A two-digit year expands by the usual convention: 69 and above is last century. |
| A spreadsheet serial number that fell into a date column | Decoded — but only inside a believable monitoring window, so an easting that landed there is refused instead of becoming a date four centuries out. |
Coordinates, and the frame they are in
The same pair of numbers means three different things depending on the frame, so the frame is decided explicitly, declared in the report, and printed on the figure. Three are read: latitude and longitude in degrees, a UTM easting and northing, and a site grid in metres or feet.
Header names decide it when they can. Failing that, magnitude does: a pair around six figures and seven figures is a UTM grid, and a pair of small numbers a hundred metres apart cannot be degrees, because nobody records a well network to two decimal places of a degree. Where the header and the numbers disagree, the numbers win and the report says which header it stopped believing.
The honest part is the site grid, and it is the commonest case on a real site. A local grid has real distances and no stated origin and no stated datum: the scale bar is right, the north arrow is right relative to the grid, and the map cannot be placed on the earth. That is said in the report rather than covered up, and it is why a site-grid file gets a plain refusal from the GeoJSON export — which has to be in degrees — with the DXF offered instead, rather than eastings written into the longitude slot.
A UTM file gets the same treatment about its zone. The zone is not in the file, it is not guessed from anything, and the report says the zone is not stated.
Latitude and longitude are the one frame that is transformed. Degrees are not a distance — at 45° a degree of longitude is 79 km and a degree of latitude is 111 km — so they are projected to metres about the centre of the well field before any interpolation, and the accuracy of that projection is printed on the figure as metres per kilometre.
| In your file | What happens to it |
|---|---|
| Columns called Latitude and Longitude | Read as degrees, with the report saying the names decided it. |
| Easting 512340, Northing 4791220 | Read as a UTM grid, with the zone left blank because it is not in the file. |
| Columns called nothing useful, at UTM magnitudes | Found as a coordinate pair by the size of the numbers. |
| X and Y a hundred metres apart | Called a site grid rather than latitude and longitude. |
| Easting (ft), Northing (ft) | A site grid in feet. Feet stay feet everywhere downstream. |
| Columns called Lat and Long holding UTM numbers | The numbers are believed, the frame is corrected, and the report says the values are outside the range those names allow. |
| One typo’d latitude of 412.5 among good ones | That row is excluded and the frame is left alone. One bad cell must not cost every other well its coordinate system. |
| A file in degrees | Projected to metres about the centre of the wells, with the accuracy stated on the figure. |
| A file already in feet on a site grid | Passed through untouched, in feet, with nothing disclosed because nothing was done. |
Wide tables and long ones
A wide table has one row per well and a column per thing measured. That is the shape a gauging spreadsheet is in and it is the shape this reads most naturally.
A long table has one row per measurement — a location, a date, an analyte name, a result, a unit, a qualifier — which is what a laboratory data deliverable looks like. It is recognised by that set of columns and turned sideways into one column per analyte, with the reshape explained in the report rather than performed quietly. The unit column travels with each analyte, and the qualifier column is applied to the result it belongs to.
A well that appears on several dates is one well. The stations list is de-duplicated by name and every reading keeps the date it was taken on, which is what lets one file draw a change map or a trend map without being rearranged first.
| In your file | What happens to it |
|---|---|
| Location, Sample Date, Analyte, Result, Units, Qualifier | Recognised as a long table and pivoted to one column per analyte. |
| Benzene and Toluene rows for the same well | Become two value columns. |
| A units column saying ug/L | Carried across to every analyte it applied to. |
| The same well, date and analyte twice | The first is kept and the repeat is named in a chip. |
| One well gauged on two dates | One station, two observations, two dates. |
| One well name at two different positions | The first position wins and the conflict is named, so a later typo cannot move a well. |
Nothing in a bad row stops the rest of the file
A row this reader cannot place is excluded and named, by the line number it has in your own copy of the file. Everything else still draws. Repeats collapse into one chip with a count instead of forty identical messages.
There are only two conditions that stop a map being drawn at all: no pair of columns that could be a coordinate, and no column of values to map. Even then the file comes back carrying its roles and its report, so the detection table still renders and you can point at the right columns.
| In your file | What happens to it |
|---|---|
| A coordinate cell containing the word oops | That row is excluded, by its own line number in your file. |
| A date cell that will not parse | The well keeps its place on the map — a bad date is not a bad position. |
| Four bad rows | One chip, naming the first few and counting the rest. |
| A file with no coordinates in it at all | Said plainly, with the roles and the report still returned so the columns can be reassigned. |
| A file with positions but nothing to contour | Said plainly rather than drawn as an empty sheet. |
What it will not do
Two hundred thousand data rows is the ceiling, and a file past it is truncated with the truncation stated rather than refused. Everything is read in this tab, so a file large enough to be uncomfortable in a browser is large enough to be uncomfortable here.
There is no shapefile reader, no GeoPackage reader, and no connection to a database. A zipped shapefile dropped in is recognised and named as one, with a sentence saying it is not built, which is more useful than a generic failure.
- Nothing is converted between units. A file mixing metres and feet in two columns you intend to subtract will produce a number, and the number will be wrong, and no test anywhere can catch that for you.
- The role inference will sometimes be wrong. That is expected rather than a defect — it is why the detection table exists and why every role is one click to change — but it does mean the table is worth reading before the figure is.
Where these claims come from
Every line in the tables above is asserted by an automated test in this site’s own suite, and the build fails when one of them stops being true. The files are listed so that somebody who wants the receipts can go and read the assertion rather than take a sentence on trust.
src/lib/__tests__/csv.test.tssrc/core/spatial/__tests__/ingest.test.tssrc/core/spatial/__tests__/roles.test.tssrc/core/spatial/__tests__/parse.test.tssrc/core/spatial/__tests__/value.test.tssrc/core/spatial/__tests__/date.test.tssrc/core/spatial/__tests__/projection.test.ts
What is stated here without a test behind it
These pages describe other people’s file formats and other people’s software as well as this site’s own behaviour, and the boundary between "there is a test for this" and "this is what the documentation says" is invisible to a reader unless somebody draws it. This is where it is drawn.
- The qualifier letters follow the EPA National Functional Guidelines set. What any particular laboratory means by a letter is that laboratory’s business, and a data package using a private flag dictionary will have letters this reader keeps verbatim and gives no meaning to.
- The two-digit year rule is the POSIX convention rather than anything specific to monitoring data. It is stated here because it is what the code does, not because it is right about your file.
- Nothing on this page has been checked against a laboratory data deliverable from a real commercial laboratory. Every fixture behind it is synthetic, because no client data of any kind is allowed into this repository.
Common questions
- Do I have to rename my columns first?
- No. The headers are matched on meaning with units and punctuation set aside, and anything not recognised is shown to you with the role it was given so you can change it. Renaming a column to match a template is exactly the work this is meant to remove.
- What if my file has eight quarters in it?
- That is the normal case and it is read as one file. Each reading keeps its date, the wells are de-duplicated, and the maps that need two dates or a whole record — the change map, the trend map — will offer you the rounds to pick from.
- Is my file uploaded anywhere?
- No. It is read in this browser tab by code already on your machine, and there is no server to send it to. Load the page, disconnect the network and keep working — the privacy page explains how to check that rather than asking you to believe it.
- Can I paste instead of choosing a file?
- Yes, and a pasted block goes through exactly the same reader. It is also how a dropped workbook is shown back to you, so an edit to what you see re-reads as ordinary text.
Where to go next
- What an Excel workbook has to look like — the parts that are only true of a workbook — sheets, serial dates, what cannot be read
- How to make a potentiometric surface map — once the file reads, this is what to do with it
- Potentiometric surface map — drop the file in and see the detection table on your own data
- Plume map — where the non-detect handling on this page actually shows up
- How the arithmetic is checked — the same idea applied to the interpolation rather than the parser
Written 2026-08-15.