Lattice Grid Buy a licence

blog

The column knows it holds metres

Building…
Loading a live grid…

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.

Read next

  • Types are not labels

    In most grids a column type picks an alignment. Here it decides storage, comparison, parsing, formatting and the editor, from one word.

  • When the grid became the interface

    Grids are sold to developers, so they are built for developers. Why Lattice Grid ships redaction, annotation and presentation tools to every user as standard.

  • Two ways to show a row's detail

    A row that expands into a nested grid, or a pane beside the list where nothing moves. They solve different problems, and one config field switches between them.

All posts RSS