developer guide
Data Grid Column Types and Formatting
Give a column a type and the grid formats, sorts, filters and totals it as that kind of value: number and currency, date, datetime and timestamp shown in whichever zone the reader works in, duration, lookup, and the technical families such as bitrate and speed.
Developer guide › Columns and cell rendering › Data Grid Column Types and Formatting
Types and formatting
A type is not a label. It is a bundle of behaviour: how a value is formatted, parsed back from
text, compared when sorting, stored in the columnar backing, written to Excel, and put on the
clipboard. Setting type configures all of that at once.
| Family | Types |
|---|---|
| Core | text, number, boolean, date, dateString, object, lookup |
| Temporal | datetime, timestamp, time, duration |
| Network | ipv4, ipv6, cidr, mac |
| Numeric bases | hex, hex8, hex16, hex32, binary, binary8, octal |
| Units: computing | bytes, megabytes, gigabytes, bitrate, gigabits |
| Units: physical | metres, millimetres, kilometres, grams, kilograms, tonnes, seconds, milliseconds, hours |
| Units: engineering | speed, kph, mph, knots, acceleration, area, hectares, volume, cubicMetres, energy, kilowattHours, power, kilowatts, force, pressure, bar, psi, torque, density, flow, litresPerMinute, radians, degrees |
| Units: electrical and scientific | voltage, current, resistance, capacitance, inductance, charge, conductance, fluxDensity, luminousFlux, illuminance, substance, absorbedDose, equivalentDose, radioactivity, luminousIntensity, doseRate, rpm, angularVelocity, ppm, ppb, basisPoints, molarity, massFlow, tonnesPerHour, viscosity, kinematicViscosity, thermalConductivity, specificHeat, frequency |
| Temperature | celsius, fahrenheit, kelvin |
| Currency | currency, usd, eur, gbp, jpy |
| Structured | json, colour, rating, percent |
Units
A unit type stores a plain number in a named base unit and makes only display and input
unit-aware. That is the whole design: the column still backs onto a typed array, so sorting,
filtering, grouping and totalling stay ordinary arithmetic and never touch the rendered text.
A gigabyte column holding 0.5 shows 512 MB, accepts
512M typed over it, and stores 0.5 throughout.
Declared types, and one built to order
columns: [
{ field: 'span', type: 'metres' }, // 1500 → "1.5 km"
{ field: 'load', type: 'kilograms' },
{ field: 'inlet', type: 'pressure' }, // 200000 → "200 kPa"
{ field: 'bearing', type: 'degrees' },
{ field: 'cap', type: 'capacitance' }, // 4.7e-11 → "47 pF"
{ field: 'inletTemp', type: 'celsius' },
],
// or configure your own base unit and precision
dataTypes: {
runtime: createUnitType({ system: 'duration', unit: 'ms', display: 'auto' }),
}
Seventeen unit systems ship: data, bitrate, length,
mass, duration, speed, acceleration,
area, volume, energy, power,
force, pressure, torque, density,
flow and angle, plus fifteen SI-prefixed electrical and scientific
quantities. registerUnitSystem adds one of your own.
Ambiguous units are refused, not guessed. A US gallon and an imperial
gallon differ by about a fifth, and "ton" means three different masses. Each has its own
symbol: gal (US), ton (UK), and the bare word is claimed by all of
them, so typing it is rejected rather than resolved. A gal silently taken as US
in a UK deployment is data corruption that reads as rounding.
The same rule catches case: mV and MV are a billion apart, so both
exact spellings work and the case-folded mv is refused.
Customary units are accepted but never chosen. display: 'auto'
walks the coherent SI ladder only. With the calorie, the BTU and the kilojoule all on one
ladder, 4,000 J would render as 3.79 BTU: auto picks the largest unit that fits,
and the BTU happens to be larger than the kilojoule. Ask for a BTU by name and you get one.
significantFigures renders to a fixed precision rather than a fixed number of
decimals. Two decimals is four significant figures at 12.34 kB and three at
1.54 kB, so a column claims different accuracy row by row depending on nothing but
which unit auto picked; significant figures are what an instrument has, and they hold across
the ladder. Rounding is applied before the unit is chosen, so 999,999 bytes to three figures
is 1.00 MB rather than 1,000 kB.
Rounding before the rung is chosen: significantFigures, executed
const { formatUnit } = await import('../packages/core/src/index.js');
const spec = { system: 'data', unit: 'B', display: 'auto', locale: 'en-GB' };
return (`figures ${formatUnit(999999, { ...spec, significantFigures: 3 })}`
+ ` | none ${formatUnit(999999, spec)}`);
compound: ['ft', 'in'] renders one stored number across an ordered subset of the
system's units - a length as 5 ft 11 in, a duration as 1 h 23 m.
It is display and parse only: the stored value stays a single base-unit number, so sorting,
filtering, grouping and totals are the same arithmetic as any other unit column. The units are
sorted largest to smallest, the smallest carries the remainder, and parsing sums the parts, so
the display round-trips through a paste. The mid-value editor - keystroke
roll-over between feet and inches, caret behaviour at a rung boundary - is a separate,
later piece of work; this is the read-and-paste half.
Angles wrap, so their mean is replaced. The average of 359° and 1° is 0°,
and the arithmetic answer (180°) is a confident, plausible number pointing in exactly the
wrong direction. A degrees or radians column averages by direction
instead, and reports nothing where the angles cancel and there is no mean direction to give.
The sum stays arithmetic, because a total rotation of 720° is two turns and that is a
real figure.
Temperature is its own type, not a unit. Every other unit is a
multiplication; Celsius to Fahrenheit carries an offset, and zero Celsius is not zero
anything, so no factor converts it. celsius, fahrenheit and
kelvin convert on input: type 72 F into a Celsius column and it
stores 22.2, and refuse to be summed: twenty degrees plus twenty degrees is
not forty degrees, and a footer saying so would be believed.
Currency is its own type, not a unit either. A currency's
“factor” is an exchange rate that moves, so a value carries an amount
and a code and never a fixed factor. The grid ships and fetches no rates: pass a
rate source through createCurrencyType({ display, rates }), and a rate that is
needed but absent renders as a loud marker (missingRate), never as zero. A
column totalling in a display currency refuses to add unlike currencies until every value
can reach that currency. The shipped currency, usd,
eur, gbp and jpy types cover the single-currency case;
a rate table is denominated in a base you can state with rateBase.
Types read from the data
A column that declares no type takes one from the rows. The first hundred non-empty
values of that column are sampled, and a type is adopted only if every one of them matches it;
anything mixed or ambiguous stays text and says so once on the console.
One key each, and the types arrive
columns: [
{ field: 'sku' }, // text
{ field: 'quantity' }, // number: aligned right, numeric filter
{ field: 'shipped' }, // date
{ field: 'expedited' } // boolean: checkbox editor
]
Sampling happens once, when rows first arrive. A grid built empty and filled later infers on that first load, so fetching after construction is no reason to declare types you would otherwise leave out. Later loads keep the types already settled on: data that arrives tomorrow cannot change a column's type under a formatter or an editor that was configured around it.
Only the built-in names are ever inferred. Candidates are tried in registration order, so every
string reaches text and every number reaches number before an extended
type is considered. A column of IP addresses or durations is text until you name the
type you want.
Inference never overrules a decision you made. An explicit type, a
preset that carries one, a type in columnDefaults, and a
lookup all win; type: false turns sampling off and keeps the column
text whatever it holds. Set sampleSize to sample more or fewer than a hundred.
What declaring a type buys you, and what it does not. Sorting is not the reason. The default comparator is value-aware, so an undeclared column of numbers already sorts 2.5 before 10 rather than lexicographically, and an undeclared column of IPv4 addresses already orders correctly across 128.0.0.1.
What a type settles is everything around the sort: which editor opens, how typed text is parsed back into a value, how the value is formatted, which filter kind the header offers, how the column is stored, and what lands in Excel and on the clipboard. That is what inference supplies for free on a plain column, and what naming a type explicitly gives you where the data cannot say it, a duration, a byte count, a network address.
Dates are stored as strings, deliberately
A date column stores '2024-03-11', not a Date. This is
the single most consequential type decision in the product.
The bug it avoids: new Date('2024-03-11') is midnight
UTC. Render that in New York and it is the 10th. A user in London sets a delivery
date, a colleague in Mumbai opens the same grid and sees the day before. Storing the wall-clock
string means the date a user typed is the date everyone sees, because there is no instant to
convert.
It is also faster and smaller: ISO 8601 sorts lexicographically in the same order it sorts chronologically, so a date column sorts as text, and repeated dates dictionary-encode well.
When you genuinely mean an instant (a log timestamp, an audit time) use
timestamp - see below. datetime stays a wall clock, deliberately.
timestamp - an instant, stored UTC, shown in a chosen zone
type: 'timestamp' is the sibling to datetime for data that is one
genuine moment everywhere - an audit time, an event created_at, a cross-region log
line - rather than a wall clock.
Stored as an instant. A value ingests from epoch-millis, a Date,
or a zone-bearing ISO string (…Z / …+01:00) and is stored as
epoch-millis UTC. Because storage is numeric, sort, filter and compare operate on the
instant, never on rendered text - two rows from different origin zones order by true
chronology, and changing the display zone never reorders them.
Shown in a display zone you control. The cell renders in the zone resolved
by precedence: the column's typeOptions.timeZone, then the grid's
config.timeZone, then the viewer's local zone. The resolved zone is nameable
(e.g. Europe/London (BST)) so a reader always knows which clock they are reading;
set typeOptions.showOrigin: true to also show the origin zone when it differs, and
when no origin was recorded the cell says so rather than assuming local.
Grouped by civil day in the display zone. Grouping a timestamp
column buckets by civil day by default - group: { granularity: 'week' | 'month' | 'instant', weekStart: 1 }
chooses week (Monday-start by default), month, or the exact instant. Buckets are computed by
projecting the instant to a civil date in the display zone, so a 23- or 25-hour daylight-saving
day still collapses to one bucket rather than splitting.
Excel export. Excel has no zone, so the display-zone wall clock is written
as a plain numeric datetime serial (yyyy-mm-dd hh:mm) - what you saw on screen,
with the zone named in the docs and column header, never shifted silently to UTC.
Formats
Shorthand and full form
format: 'currency:GBP:2' // £1,234.50
format: 'percent:1' // 87.4%
format: 'date:dd MMM yyyy' // 11 Mar 2024
format: { // when the shorthand runs out
type: 'number',
style: 'currency', currency: 'GBP', decimals: 2,
negative: 'parentheses', // (£1,234.50)
negativeClass: 'is-loss',
nullDisplay: ', ',
}
format: { decimals: 0, signed: true } // +5, 0, -5
The shorthand grammar. A shorthand is style, or
style:arg1:arg2 - colon-separated, every argument after the style optional. Seven
styles are recognised, by their leading word: number:decimals
('number:0' → 1,235), percent:decimals
('percent:1' → 87.4%), currency:code:decimals
('currency:GBP:2' → £1,234.50; the code defaults to USD
and decimals to the currency's own minor unit when omitted),
date:pattern ('date:dd MMM yyyy'; everything after the first colon is
the pattern, so a time pattern's own colons - 'date:HH:mm' - survive intact),
relative (no argument: '3 days ago', re-rendering as the clock moves),
boolean:trueLabel/falseLabel/nullLabel (slash-separated, not colon - 'boolean:Yes/No' or 'boolean:Yes/No/Unknown'), and
text:transform:truncate ('text:upper:20'; transform is
upper, lower or title). A string that does not start
with one of these seven words is tried as an Excel-style mask
('£#,##0.00;(£#,##0.00)') and then as a bare date pattern
('dd MMM yyyy') before being rejected.
signed puts a leading + on a positive number, currency or percent
value. Zero gets no sign either way - it is neither positive nor negative - and a negative
value is entirely unaffected: it still renders however negative says
('minus' by default). Off by default, so an existing column's negatives-only
look never changes underneath it.
A key in format that the resolved type does not read - a typo, or a key that
belongs to a different format shape, such as signed written on a
text column - is never silently dropped. It warns once, by column and key name, instead of
doing nothing.
Lookups
A lookup column stores an id and shows a label. Sorting, filtering, grouping, copying and exporting all use the label, because that is the thing the user is reasoning about, but the data keeps the id.
A status column
{
field: 'statusId',
title: 'Status',
type: 'lookup',
lookup: { options: [
{ id: 1, label: 'Open' },
{ id: 2, label: 'Pending' },
{ id: 3, label: 'Closed' },
{ id: 4, label: 'Escalated', variant: 'danger' },
]},
cell: { decoration: 'pill' },
}
Options may be a function, may return a promise, and may be searched remotely with an
AbortSignal so a superseded keystroke cancels its own request.
Custom types
A 32-bit register column
dataTypes: {
reg32: LatticeGrid.createRadixType({
radix: 2, bitWidth: 32, pad: true, signed: false, group: 8,
}),
},
columns: [{ field: 'flags', type: 'reg32' }],
// 170 → 0b00000000 00000000 00000000 10101010
radix is 2, 8 or 16, or the names binary,
octal and hex. Base 10 is not among them and is not an oversight:
a decimal number is what the number type is for, with grouping, decimals,
currency and notation that a radix formatter has no concept of. Passing
radix: 10 names the supported set in the console and falls back to hex rather
than producing something that looks like a number column but is not one.