# Merged cells

> Merge repeated values down a column, or let a cell span several columns, without changing the data underneath.

Merged cells remove repetition that hides structure. On a blotter, the account, order and symbol repeat down every fill; merged, each shows once, and the order boundaries stand out. A cell can also span columns to the right, for a note that needs the room. Merging changes only what is drawn: every row keeps its own values.

Account, order and symbol merge down their runs; the note row spans the Qty and Price columns:

Example: Merging down and across (merged-cells.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef } from 'bo-grid';

  // Fills of a few orders: account, order and symbol repeat down the rows.
  let rows = $state([
    { id: 1, account: 'ACC-01', order: 'O-1001', symbol: 'VNM', time: '09:15:02', qty: 500, price: 68.4 },
    { id: 2, account: 'ACC-01', order: 'O-1001', symbol: 'VNM', time: '09:15:09', qty: 700, price: 68.5 },
    { id: 3, account: 'ACC-01', order: 'O-1002', symbol: 'FPT', time: '09:16:40', qty: 300, price: 132.0 },
    { id: 4, account: 'ACC-01', order: 'O-1002', symbol: 'FPT', time: '09:16:41', note: 'Order amended: limit raised to 132.2' },
    { id: 5, account: 'ACC-02', order: 'O-1003', symbol: 'HPG', time: '09:20:11', qty: 2000, price: 27.3 },
    { id: 6, account: 'ACC-02', order: 'O-1003', symbol: 'HPG', time: '09:20:15', qty: 3000, price: 27.35 },
    { id: 7, account: 'ACC-02', order: 'O-1004', symbol: 'VCB', time: '09:31:58', qty: 800, price: 91.5 },
  ]);

  const columns: ColumnDef[] = [
    // `spanRows`: equal values in adjacent rows merge into one cell.
    { type: 'text', key: 'account', header: 'Account', width: 100, spanRows: true },
    { type: 'text', key: 'order', header: 'Order', width: 96, spanRows: true },
    { type: 'text', key: 'symbol', header: 'Symbol', width: 84, spanRows: true },
    { type: 'text', key: 'time', header: 'Time', width: 90 },
    // `colSpan`: a note row covers the fill columns to its right.
    {
      type: 'text',
      key: 'qtyText',
      header: 'Qty',
      width: 90,
      value: (r) => r.note ?? String(r.qty),
      colSpan: (r) => (r.note ? 2 : 1),
    },
    { type: 'price', key: 'price', header: 'Price', flex: 1 },
  ];
</script>

<Grid {rows} {columns} height={280} />
```

## Down a column: spanRows

`spanRows: true` merges a cell over adjacent rows with the same value. Blank values never merge.

```ts
const columns: ColumnDef[] = [
  { type: 'text', key: 'account', header: 'Account', width: 100, spanRows: true },
  { type: 'text', key: 'order', header: 'Order', width: 96, spanRows: true },
  { type: 'text', key: 'symbol', header: 'Symbol', width: 84, spanRows: true },
  { type: 'text', key: 'time', header: 'Time', width: 90 },
  { type: 'price', key: 'price', header: 'Price', flex: 1 },
];
```

To decide per pair of adjacent rows instead, pass a function, for example to merge dates on the same calendar day:

```ts
{ type: 'date', key: 'filledAt', header: 'Day', spanRows: (a, b) => sameDay(a.filledAt, b.filledAt) }
```

- **Hierarchical, in display order.** A run also breaks wherever a merging column to its left breaks, so a symbol never merges across two accounts. Moving columns changes the hierarchy.
- **Only adjacent rows merge**, so merging follows the current sort: sort by the merged column for the largest merges. Group headers, tree placeholders and an expanded detail row break every run.
- **A drawing rule, not a data change.** Sorting, filtering, copying, CSV and Excel export, and `toHTMLTable` all see one value per row.
- A merged cell is not editable. Selecting any row in a run highlights the cell.
- Runs are recomputed when the view changes, like sorting and filtering, not on every realtime tick.

## Across columns: colSpan

`colSpan(row)` returns how many columns a cell covers in that row; the covered cells are not drawn. A note row, for example, can cover the fill columns:

```ts
{
  type: 'text',
  key: 'qtyText',
  header: 'Qty',
  value: (r) => r.note ?? String(r.qty),
  colSpan: (r) => (r.note ? 2 : 1),
}
```

- The leftmost claim wins when spans overlap.
- A span stops at the last column and at the edge between pinned and scrolling columns.
- A covered cell breaks the `spanRows` run of the column it covers, but not of a column to its left. On one column with both, a run continues only while the rows below span the same width.
- `colSpan` is ignored when `virtualizeColumns` is on.

## Both are in memory only

Merging needs neighbouring rows, so it applies to rows in memory. With a server [data source](https://bonguynvan.github.io/bo-grid/docs/server-data/), which holds one window of rows at a time, cells don't merge.

The planning is a pure function, `buildMergePlan(...)` with `colSpanRow(...)`, exported for anything that draws its own view of the same rows.

## In the gallery

A trade blotter with merged account, order and symbol runs, and note rows across the fill columns:

Live example: https://bonguynvan.github.io/bo-grid/examples/blotter/ (source: https://github.com/bonguynvan/bo-grid/blob/main/src/demo/examples/)

## Reference

- `spanRows`: `boolean | ((a: GridRow, b: GridRow) => boolean)` (default `false`) — Merge this column's cell down over adjacent rows. `true` joins rows with equal (non-blank) values; a function decides for each adjacent pair. Hierarchical: a run also breaks wherever a spanning column to its left breaks, and at group headers. A drawing rule only — sort, filter, copy and export still see every row's own value. In-memory mode only.

  ```ts
  { type: 'text', key: 'account', header: 'Account', spanRows: true }
  ```

- `colSpan`: `(row: GridRow) => number` — How many columns this cell covers for a given row (1 when unset). Covered cells are not drawn. Clamped at a pinned/scrolling boundary and the last column; the leftmost claim wins. Ignored under `virtualizeColumns`.

  ```ts
  { type: 'text', key: 'sym', header: 'Symbol', colSpan: (row) => (row.kind === 'note' ? 4 : 1) }
  ```

---
Page: https://bonguynvan.github.io/bo-grid/docs/merged-cells/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
