The column knows it holds metres
Every data grid can right-align a number and stick “kg” on the end. That is a formatter, and a formatter is a lie you tell the reader on the way out of the database.
Here is how you find out it was a lie. You have a column of file sizes. You have
been careful: you store bytes, you format them nicely, and the cells read
1.54 kB, 900 MB, 1 GB, 2 TB. Now the user clicks the header to sort.
If the grid sorts the number, they get what they expect:
1.54 kB < 900 MB < 1 GB < 2 TB
If anything in the pipeline touched the display text, a formatter, a cell renderer, a CSV export that re-read the DOM, they get this:
1 GB < 1.54 kB < 2 TB < 900 MB
Because “1” sorts before “9”. It is the oldest bug in tabular software and it survives because it looks fine until someone sorts.
Units are a type, not a decoration
Lattice Grid treats a quantity as a data type, which means the grid commits to
one thing and refuses to negotiate: the stored value is always a plain number in
the column’s base unit. A gigabytes column holds 10. A metres column holds
2.5. Nothing else in the system ever sees anything but a number.
That constraint is what makes everything else work. The column still backs onto
a Float64Array. The radix sort works. The filter operators work. Grouping,
totals, the pivot, the statistics kernels: none of them know units exist, and
none of them need to. Only two places in the whole system are unit-aware:
display, and input.
An editor alone could not do this. An editor that shows 1 GB still leaves the
grid sorting a string.
Display: pick the scale that suits the value
Set display: 'auto' and each cell renders at whatever rung of the ladder makes
it readable. That is the grid above: one metres column, shown five ways, and
every one of them sorts in the same order because the stored metres never
changed. Same column, same stored numbers, all metres:
stored 0.0004 -> 400 µm
stored 0.05 -> 5 cm
stored 2.5 -> 2.5 m
stored 1200 -> 1.2 km
Four orders of magnitude, one column, no conditional formatting, no per-row logic. The value never changed: 1200 is still 1200 metres, and it still sorts after 2.5.
Input: accept what the user actually types
This is the half people forget. A user who works in millimetres types millimetres. A user reading a spec sheet types what is on the spec sheet. Into that same metres column:
"50mm" -> 0.05 m
"1 km" -> 1000 m
"3 ft" -> 0.9144 m
"0.5 mi" -> 804.672 m
"8 nmi" -> 14816 m
"2.5" -> 2.5 m
Imperial into a metric column, and no conversion code anywhere in the
application. A mass column storing kilograms takes 14 st and stores 88.9041. A
duration column storing seconds takes 90m, 1.5h, 250ms and 2d.
The parsing grammar lives in the type, not in the editor, deliberately, because
the editor, the filter’s value box and the clipboard paste parser all have to
agree about what 10M means. Three implementations of that question is three
answers, and two of them are wrong.
The 10M problem
Speaking of which. On a data column, what does a user mean when they type 10M?
Ten megabytes, if they think in decimal. Ten mebibytes, if they think in powers of two. The honest answer is that the input is ambiguous and the column knows something the user did not say.
bare "10M" on a decimal column -> 10,000,000 bytes
bare "10M" on a binary column -> 10,485,760 bytes
The column’s ladder settles it. But, and this is the part worth stealing, an explicit symbol is never reinterpreted:
"10MB" typed into a binary column -> 10,000,000 bytes
"10MiB" typed into a decimal column -> 10,485,760 bytes
Configuration resolves ambiguity only where the user was actually ambiguous.
When they said which unit they meant, they get that unit, whatever the column
prefers. A system that “helpfully” corrected 10MiB to 10 MB on a decimal
column would be wrong, and wrong by 5%, and silent about it.
Twenty-six systems, or one of your own
There are 26 built-in unit systems carrying 173 symbols: length, mass, duration, data, bitrate, speed, acceleration, area, volume, energy, power, force, pressure, torque, density, flow, angle, and the scientific ones through to thermal conductivity and dose rate.
That will not cover your domain. It cannot: your domain has a quantity nobody else measures.
So the same door the built-ins came through is open:
import { registerUnitSystem, defineUnit, createUnitType }
from '@toclocoinc/lattice-grid';
// Yarn count. The base quantity is tex: grams per 1000 metres.
registerUnitSystem('yarn', [
defineUnit('dtex', 0.1, ['decitex']),
defineUnit('tex', 1, []),
defineUnit('ktex', 1000, [], { prefix: 'k' }),
]);
const linear = createUnitType({
system: 'yarn', unit: 'tex', display: 'auto', maxDecimals: 2,
});
const grid = LatticeGrid.createGrid(el, {
dataTypes: { linear },
columns: [{ field: 'd', title: 'Linear density', type: 'linear', total: 'sum' }],
rows,
});
Three lines of declaration, and the column now behaves exactly like the built-in
ones. It stores tex. It displays 4 dtex and 2.4 ktex from the same column. It
accepts 33 dtex and 1.2ktex typed in. It sorts, filters, groups and totals on
the number, because it never stopped being a number.
A unit is a symbol and a factor into the system’s base quantity. That is the whole model. It is small enough to hold in your head, which is why the same three calls describe yarn count, or basis points, or a currency ladder where k£ and M£ sit above the pound.
Two decisions worth explaining
Registering over an existing system throws. You cannot quietly redefine
length. Shadowing would change what every column already using that name means,
and the failure would arrive as numbers wrong by a constant factor rather than as
an error anyone could trace. Loud beats convenient.
Temperature is not in the ladder. A unit here is a multiplicative factor, and °C to °F is affine: there is an offset, not just a scale. Rather than bend the model until it half-fits, temperature is its own set of types with its own editor. A model that cannot express something should say so instead of being approximately right.
Why this matters more than it sounds
The reason to care is not that 1.2 km reads better than 1200. It is that the
alternative is application code.
Without unit types, every one of these lives in your codebase: a formatter per
column, a parser for the edit path, a second parser for pasted values, a
conversion table, a comparator so sorting still works, and a decision, usually
made twice, differently, about what the user meant by 10M. All of it written
once per project, none of it interesting, and every piece a place for the display
and the stored value to drift apart.
Push it into the type and there is one answer to each question, in one place, shared by the editor, the filter, the clipboard and the exporter. The grid keeps a number. The user keeps their own units. Nobody has to write the conversion table again.