# Conditional formatting

> Paint data bars, icons and colour scales into numeric cells from the values themselves, with no custom cell code.

Conditional formatting draws a cue into a numeric cell from its value: a bar behind it, an icon beside it, or a background tint. It answers "which rows stand out?" at a glance, and needs no custom cell. All three work on any numeric column and can be combined.

Sort by Volume and watch the bars line up; the icons and the tint follow the Chg % values:

Example: Icons, a colour scale and data bars (conditional-formatting.svelte)

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

  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 90 },
    // An icon picked by threshold: the highest `at` not above the value wins.
    {
      type: 'percent',
      key: 'change',
      header: 'Chg %',
      width: 110,
      icons: [
        { at: -Infinity, icon: '▼', tone: 'down' },
        { at: -0.25, icon: '●', tone: 'neutral' },
        { at: 0.25, icon: '▲', tone: 'up' },
      ],
    },
    // A background tint across the column's range, diverging around zero.
    { type: 'percent', key: 'change', header: 'Heat', width: 96, colorScale: { mid: 0 } },
    // A bar behind the value, proportional from zero.
    { type: 'volume', key: 'volume', header: 'Volume', width: 150, dataBar: { min: 0 } },
    { type: 'price', key: 'price', header: 'Price', flex: 1 },
  ];

  let rows = $state(quotes());
</script>

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

data.ts:

```ts
// Small, fixed data for the docs' examples: every example reads the same
// symbols, so a reader compares features, not datasets.
// A type, not an interface: it then fits GridRow's index signature.
export type Quote = {
  id: number;
  symbol: string;
  name: string;
  sector: string;
  price: number;
  change: number;
  volume: number;
};

const BASE: Omit<Quote, 'id'>[] = [
  { symbol: 'VNM', name: 'Vinamilk', sector: 'Consumer', price: 68.5, change: 1.2, volume: 1_204_000 },
  { symbol: 'FPT', name: 'FPT Corp', sector: 'Technology', price: 132, change: -0.4, volume: 860_500 },
  { symbol: 'HPG', name: 'Hoa Phat', sector: 'Materials', price: 27.35, change: 0.74, volume: 3_412_900 },
  { symbol: 'VCB', name: 'Vietcombank', sector: 'Financials', price: 91.5, change: 0.55, volume: 1_986_200 },
  { symbol: 'MWG', name: 'Mobile World', sector: 'Consumer', price: 51.4, change: -1.1, volume: 2_250_400 },
  { symbol: 'SSI', name: 'SSI Securities', sector: 'Financials', price: 33.3, change: -0.45, volume: 4_104_700 },
  { symbol: 'VIC', name: 'Vingroup', sector: 'Real estate', price: 44.15, change: -0.11, volume: 1_530_100 },
  { symbol: 'GAS', name: 'PV Gas', sector: 'Energy', price: 71.8, change: 0.3, volume: 645_800 },
  { symbol: 'MSN', name: 'Masan', sector: 'Consumer', price: 76.2, change: 2.05, volume: 1_120_300 },
  { symbol: 'TCB', name: 'Techcombank', sector: 'Financials', price: 23.9, change: 0.84, volume: 5_870_000 },
  { symbol: 'VHM', name: 'Vinhomes', sector: 'Real estate', price: 41.6, change: -0.72, volume: 2_904_600 },
  { symbol: 'POW', name: 'PV Power', sector: 'Energy', price: 12.25, change: 1.66, volume: 6_315_200 },
];

/** A fresh copy of the quotes, so one example's edits never reach another. */
export function quotes(): Quote[] {
  return BASE.map((q, i) => ({ id: i + 1, ...q }));
}
```

## Data bars

`dataBar` paints a horizontal bar behind the value, scaled across the column's range.

- The range is computed over the rows in view, unless you give `min` and `max`. `min: 0` gives bars proportional to the value.
- When the range spans negative values, bars grow left and right from a zero line.
- `color` and `negative` override the default up and down colours.

```ts
{ type: 'volume', key: 'volume', header: 'Volume', dataBar: { min: 0 } }
{ type: 'number', key: 'pnl', header: 'P&L', decimals: 0, dataBar: {} } // diverging around zero
```

## Icons

`icons` shows an icon beside the value. Each rule has a threshold `at`, an `icon` and a `tone` (`up`, `down`, `amber`, `info` or `neutral`) for its colour. The rule with the highest `at` that is not above the value wins:

```ts
{
  type: 'percent',
  key: 'change',
  header: 'Chg %',
  icons: [
    { at: -Infinity, icon: '▼', tone: 'down' },
    { at: -0.25, icon: '●', tone: 'neutral' },
    { at: 0.25, icon: '▲', tone: 'up' },
  ],
}
```

## Colour scales

`colorScale` tints the cell background across the column's range: a soft ramp in the theme's colours.

- It is computed over the rows in view, unless you give `min` and `max`.
- `mid` makes a three-stop scale that diverges at that value, such as `mid: 0` for gains and losses.
- `colors` sets the stops: `[low, high]`, or `[low, mid, high]` with `mid`.

```ts
{ type: 'number', key: 'pnlPct', header: 'P&L %', decimals: 1, colorScale: { mid: 0 } }
```

The `heatmap` column type is the fixed alternative: a ramp between a `min` and a `max` you set, with the value written on top.

## With live data and server data

The cues follow the values, so they update with a live feed and work alongside [cell flashing](https://bonguynvan.github.io/bo-grid/docs/flashing/). Automatic ranges are computed over the rows in memory; with a server [data source](https://bonguynvan.github.io/bo-grid/docs/server-data/), which only holds one window of rows at a time, give explicit `min` and `max`.

## In the gallery

A correlation matrix drawn as a heat map, with a pinned label column:

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

## Reference

- `dataBar`: `DataBarConfig` (default `undefined`) — Conditional formatting — a horizontal bar painted behind the cell value, scaled across the column's value range. The range is auto-computed over the current view (in-memory) unless `min`/`max` are given; pass `min: 0` for absolute proportional bars. Bars cross a zero baseline when the range spans negatives. Numeric columns only.

  ```ts
  { type: 'volume', key: 'vol', header: 'Volume', dataBar: { min: 0 } }
  ```

- `icons`: `IconRule[]` (default `undefined`) — Conditional formatting — show an icon beside the value, chosen by the highest threshold `at` that is ≤ the cell value (e.g. ▲ ● ▼ by sign or band). Numeric columns only.

  ```ts
  { type: 'percent', key: 'chg', header: 'Chg %', icons: [{ at: -Infinity, icon: '▼', tone: 'down' }, { at: 0, icon: '●' }, { at: 0.01, icon: '▲', tone: 'up' }] }
  ```

- `colorScale`: `ColorScaleConfig` (default `undefined`) — Conditional formatting — tint the cell background across the column's value range (a soft heat ramp). Auto-ranged over the current view unless `min`/ `max` are given; pass `mid` for a 3-stop diverging scale (e.g. `mid: 0`). Numeric columns only.

  ```ts
  { type: 'percent', key: 'chg', header: 'Chg %', colorScale: { mid: 0 } }
  ```

- `cellClass`: `string | ((value: unknown, row: GridRow) => string | undefined)` (default `undefined`) — Extra class(es) for this column's data cells — a static string, or a function of the cell value/row for conditional styling. Target via `:global(.bo-grid .c.your-class)`.

  ```ts
  { type: 'price', key: 'last', header: 'Last', cellClass: (v, row) => (Number(v) >= Number(row.ceiling) ? 'at-ceiling' : undefined) }
  ```

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