Lattice Grid Buy a licence

demo D24

Units in htmx

Bytes, distances and rates, parsed in the unit the user typed

createUnitType()

This grid understands the unit a value carries, bytes, distances or rates, and parses what the user types in that unit. It keeps a column meaning a real quantity rather than a bare number, so '1.5 GB' or '40 km/h' is understood, sorted and formatted correctly.

Building…
Loading a live grid…

This is the htmx version. A server-rendered table is upgraded in place, so the page is readable before any script runs and gains the full grid when the module loads, which suits a progressively enhanced app.

The configuration

<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">

<!-- A real, server-rendered table: readable with JavaScript off. data-field on
     each header names the column that cell becomes, and data-id on each row is
     the key the grid carries it under. -->
<table id="units-table" data-lattice-grid>
  <thead>
    <tr>
      <th data-field="hostname">Hostname</th>
      <th data-field="storageBytes">Storage, IEC</th>
      <th data-field="linkBps">Link rate</th>
      <th data-field="cableMetres">Cable run</th>
      <th data-field="ports">Ports</th>
    </tr>
  </thead>
  <tbody>
    <tr data-id="1"><td>peering-0001.london.example.net</td><td>4024986955940</td><td>40000000000</td><td>3625</td><td>8</td></tr>
    <tr data-id="2"><td>leaf-0002.london.example.net</td><td>6959737882</td><td>1000000000</td><td>2444</td><td>12</td></tr>
    <tr data-id="3"><td>peering-0003.zurich.example.net</td><td>6012419053956</td><td>10000000000</td><td>1054.3</td><td>8</td></tr>
    <!-- the rest of the rows, rendered by your server -->
  </tbody>
</table>

<script type="module">
  import { hydrateTable, createUnitType } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/htmx.esm.min.js';

  // The markup carries the rows; a server-rendered table's column types are
  // declared in the config hydrateTable takes. The grid replaces the table
  // where it stood.
  const grid = hydrateTable(document.getElementById('units-table'), {
    toolPanel: {
      side: 'left',
      panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
      exportName: 'lattice-demo',
    },
    // Three unit systems, each one a call. display: 'auto' walks the ladder per
    // value, so a 900 MB row and a 4 TB row are both readable in the same
    // column without either losing its precision.
    dataTypes: {
      binaryBytes: createUnitType({ system: 'data', unit: 'B', display: 'auto', binary: true, decimals: 1 }),
      siBytes: createUnitType({ system: 'data', unit: 'B', display: 'auto', decimals: 1 }),
      fixedGiB: createUnitType({ system: 'data', unit: 'B', display: 'GiB', decimals: 2 }),
      rate: createUnitType({ system: 'bitrate', unit: 'bps', display: 'auto', decimals: 0 }),
      cable: createUnitType({ system: 'length', unit: 'm', display: 'auto', decimals: 1 }),
    },
    columns: [
      { field: 'hostname', title: 'Hostname', layout: { width: 280, pin: 'start' } },
      { field: 'storageBytes', id: 'iec', title: 'Storage, IEC', type: 'binaryBytes', layout: { width: 150 }, total: 'sum' },
      { field: 'storageBytes', id: 'si', title: 'Storage, SI', type: 'siBytes', layout: { width: 150 } },
      // The same field again, pinned to one unit, which is what a column you are
      // going to compare down rather than read across wants.
      { field: 'storageBytes', id: 'gib', title: 'Storage, GiB', type: 'fixedGiB', layout: { width: 150 } },
      { field: 'linkBps', title: 'Link rate', type: 'rate', layout: { width: 150 } },
      { field: 'cableMetres', title: 'Cable run', type: 'cable', layout: { width: 140 }, total: 'sum' },
      // The shipped types are the same call with the arguments filled in.
      { field: 'storageBytes', id: 'shipped', title: "type: 'bytes'", type: 'bytes', layout: { width: 150 } },
      { field: 'ports', title: 'Ports', type: 'number', layout: { width: 110 } },
    ],
  });
</script>

Parsing and rendering unit-bearing values in a column

A JavaScript data grid that stores plain numbers loses information the moment a column actually means bytes, kilometres or requests per second: a user typing 1.5 GB or 40 km/h expects the grid to understand the unit as well as the number. Lattice Grid handles this with createUnitType(), which builds a column type from a set of unit definitions, a base unit to store values in, and the parsing rules mapping typed input to that base. A file-size column defined this way accepts 1.5 GB, 800 MB or a bare byte count on entry, stores everything internally as bytes, and displays each value back in whichever unit keeps the number readable, so a byte count in the millions renders as megabytes rather than a long string of digits.

A developer reaches for this whenever a column mixes magnitudes a single fixed format cannot cover, such as network throughput or distance travelled, where the smallest unit makes large values unreadable and the largest rounds small values to zero. Parsing resolves to the base unit once, on entry, so sorting and filtering operate on that consistent numeric value rather than the display string, and a column mixing 2 TB and 500 MB sorts correctly without a custom comparator. Rendering applies only to rows in the current virtualised viewport, so adding a unit type carries no cost for rows scrolled out of view.

How do you show file sizes or distances in a JavaScript data grid?

Define the column type with createUnitType(), supplying the units it should recognise and the base unit values are stored in. Lattice Grid parses typed input such as 1.5 GB or 40 km into that base unit, keeps sorting and filtering consistent against the stored value, and renders each cell back in whichever unit best fits its magnitude.