demo D68
Your own total function in htmx
A weighted mean or a median, and why it re-reduces
total: (values, ctx) => …
This grid computes a footer with your own function, a weighted mean or a median, and re-reduces it correctly as the rows change. It covers the totals the built-ins cannot, so a summary row can carry the exact figure your domain needs.
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="custom-totals-table" data-lattice-grid>
<thead>
<tr>
<th data-field="account">Account</th>
<th data-field="service">Service</th>
<th data-field="cost">Monthly cost</th>
<th data-field="change">Weighted change</th>
</tr>
</thead>
<tbody>
<tr data-id="1"><td>Legacy (Do Not Decommission)</td><td>Relational Database</td><td>31612.55</td><td>-0.0468</td></tr>
<tr data-id="2"><td>Cost Optimisation Programme</td><td>Elastic Compute</td><td>6109.09</td><td>0.1143</td></tr>
<tr data-id="3"><td>Data & Insights</td><td>Block Storage</td><td>42.69</td><td>0.1082</td></tr>
<!-- the rest of the rows, rendered by your server -->
</tbody>
</table>
<script type="module">
import { hydrateTable } 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('custom-totals-table'), {
grandTotalRow: 'bottom',
toolPanel: {
side: 'left',
panels: ['columns', 'filters', 'views', 'quick'],
actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
exportName: 'lattice-demo',
},
columns: [
{ field: 'account', title: 'Account', filter: { type: 'set' }, group: { enabled: true, index: 0 } },
{ field: 'service', title: 'Service', filter: { type: 'set' } },
{ field: 'cost', title: 'Monthly cost', type: 'number', layout: { width: 170 },
format: { style: 'currency', currency: 'USD', decimals: 2 }, total: 'sum' },
{
// Three columns read the same cost field, so the extra two are named:
// an id defaults to the field, and two columns cannot share one.
id: 'costMedian', field: 'cost', title: 'Median cost', type: 'number',
layout: { width: 160 }, format: { style: 'currency', currency: 'USD', decimals: 2 },
// A reduction the built-in set does not cover. It receives the column's
// values for the group being reduced, and is called again for the total.
total: (values) => {
const nums = values.filter((v) => typeof v === 'number' && Number.isFinite(v))
.sort((a, b) => a - b);
if (!nums.length) return null;
const mid = nums.length >> 1;
return nums.length % 2 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
},
},
{
id: 'costP95', field: 'cost', title: 'p95 cost', type: 'number',
layout: { width: 150 }, format: { style: 'currency', currency: 'USD', decimals: 2 },
total: (values) => {
const nums = values.filter((v) => typeof v === 'number' && Number.isFinite(v))
.sort((a, b) => a - b);
if (!nums.length) return null;
return nums[Math.min(nums.length - 1, Math.floor(nums.length * 0.95))];
},
},
{
field: 'change', title: 'Weighted change', type: 'number',
layout: { width: 180 }, format: { style: 'percent', decimals: 2 },
// A mean weighted by each row's own value.
total: (values) => {
let weight = 0;
let sum = 0;
for (const v of values) {
if (typeof v !== 'number' || !Number.isFinite(v)) continue;
const w = Math.abs(v);
weight += w;
sum += v * w;
}
return weight ? sum / weight : null;
},
},
],
});
</script>
Writing a custom total function for weighted means and medians
The built-in aggregations, sum, average, min, max, and count, cover most footers, but a weighted mean, a median, or a percentile needs its own logic, and Lattice Grid exposes that through the total column option as a function: total: (values, ctx) => …. A developer reaches for this whenever a plain average would misrepresent the data, such as an order value weighted by quantity, or a latency column where the median is a more honest summary than the mean because a handful of slow requests would skew it. The function receives the array of values in scope, group or grand total, plus a context object carrying the row data for that scope, so a weighted mean can divide a weighted sum by a total weight drawn from a second column. This is also where “why it re-reduces” matters: a custom total cannot be built incrementally from a parent and a delta the way sum can, so Lattice Grid recomputes it from the full value set on every change to that scope rather than adjusting a running figure. For a JavaScript data grid handling frequent updates, that recomputation is bounded to the rows within the affected group, not the whole dataset, so a custom median on a group of a few hundred rows stays cheap while other groups update independently.
How do I calculate a weighted average in a grid footer?
Set the column’s total to a function, total: (values, ctx) => …, rather than the string 'average'. Inside it, use ctx to reach the weighting column’s values for the same rows, multiply each value by its weight, sum the products, and divide by the sum of weights. Lattice Grid calls this function for every group footer and grand total row that includes the column.