Skip to content

Formatting Presets

Formatting presets help Pivot reports show numbers, money, percentages, and dates in a readable way. Conditional formatting presets then help users spot high revenue, low margin, or other important analytical values.

Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import type { ColumnType, ColumnTypes, DataType } from '@revolist/revogrid';
import {
  createPivotConditionalCellProperties,
  createPivotValueFormatter,
  formatPivotValue,
  PivotPlugin,
  pivotConditionalFormattingPresets,
  type PivotConfig,
} from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';

export interface PivotFormattingPresetRow {
  region: string;
  closeDate: string;
  segment: string;
  revenue: number;
  marginRate: number;
  orders: number;
}

const currencyFormatter = createPivotValueFormatter({
  preset: 'currency',
  currency: 'USD',
  locale: 'en-US',
  options: { maximumFractionDigits: 0 },
});

const percentFormatter = createPivotValueFormatter({
  preset: 'percent',
  locale: 'en-US',
  options: { minimumFractionDigits: 1, maximumFractionDigits: 1 },
});

const dateFormatter = createPivotValueFormatter({
  preset: 'date',
  locale: 'en-US',
  options: { month: 'short', day: '2-digit', year: 'numeric' },
});

const integerFormatter = createPivotValueFormatter({
  preset: 'number',
  locale: 'en-US',
  options: { maximumFractionDigits: 0 },
});

function formattedColumn(formatter: (value: unknown) => string): ColumnType {
  return {
    cellTemplate: (_h, props) => formatter(props.value),
    cellProperties: () => ({ class: { 'align-right': true } }),
  };
}

export const PIVOT_FORMATTING_PRESETS_ROWS: PivotFormattingPresetRow[] = [
  { region: 'North America', closeDate: '2026-01-16', segment: 'Software', revenue: 184000, marginRate: 0.37, orders: 32 },
  { region: 'North America', closeDate: '2026-01-22', segment: 'Services', revenue: 126000, marginRate: 0.31, orders: 27 },
  { region: 'North America', closeDate: '2026-02-10', segment: 'Software', revenue: 211000, marginRate: 0.41, orders: 35 },
  { region: 'North America', closeDate: '2026-02-18', segment: 'Hardware', revenue: 94000, marginRate: 0.22, orders: 18 },
  { region: 'EMEA', closeDate: '2026-01-19', segment: 'Software', revenue: 156000, marginRate: 0.34, orders: 29 },
  { region: 'EMEA', closeDate: '2026-02-06', segment: 'Services', revenue: 118000, marginRate: 0.28, orders: 24 },
  { region: 'EMEA', closeDate: '2026-02-24', segment: 'Hardware', revenue: 87000, marginRate: 0.19, orders: 16 },
  { region: 'APAC', closeDate: '2026-01-28', segment: 'Software', revenue: 142000, marginRate: 0.36, orders: 26 },
  { region: 'APAC', closeDate: '2026-02-12', segment: 'Services', revenue: 132000, marginRate: 0.33, orders: 31 },
  { region: 'APAC', closeDate: '2026-02-26', segment: 'Hardware', revenue: 76000, marginRate: 0.21, orders: 15 },
];

export const PIVOT_FORMATTING_PRESETS_CELL_PROPERTIES = createPivotConditionalCellProperties(
  [
    pivotConditionalFormattingPresets.gt(300000, {
      field: 'revenue',
      class: 'pivot-formatting-strong',
      style: {
        backgroundColor: 'rgba(22, 163, 74, 0.16)',
        color: '#166534',
        fontWeight: '600',
      },
    }),
    pivotConditionalFormattingPresets.lt(0.28, {
      field: 'marginRate',
      class: 'pivot-formatting-risk',
      style: {
        backgroundColor: 'rgba(220, 38, 38, 0.14)',
        color: '#991b1b',
        fontWeight: '600',
      },
    }),
    pivotConditionalFormattingPresets.between(0.28, 0.34, {
      field: 'marginRate',
      style: {
        backgroundColor: 'rgba(234, 179, 8, 0.14)',
        color: '#854d0e',
      },
    }),
    pivotConditionalFormattingPresets.gt(55, {
      field: 'orders',
      style: {
        backgroundColor: 'rgba(37, 99, 235, 0.14)',
        color: '#1d4ed8',
        fontWeight: '600',
      },
    }),
  ],
  { match: 'all' },
);

export const PIVOT_FORMATTING_PRESETS_COLUMN_TYPES: ColumnTypes = {
  pivotCurrency: formattedColumn(currencyFormatter),
  pivotPercent: formattedColumn(percentFormatter),
  pivotDate: {
    cellTemplate: (_h, props) => dateFormatter(props.value),
  },
  pivotInteger: formattedColumn(integerFormatter),
};

export const PIVOT_FORMATTING_PRESETS_CONFIG: PivotConfig = {
  dimensions: [
    { prop: 'region', name: 'Region', sortable: true, size: 170 },
    { prop: 'closeDate', name: 'Close date', columnType: 'pivotDate', sortable: true, size: 140 },
    { prop: 'segment', name: 'Segment', sortable: true, size: 140 },
    {
      prop: 'revenue',
      name: 'Revenue',
      columnType: 'pivotCurrency',
      cellProperties: PIVOT_FORMATTING_PRESETS_CELL_PROPERTIES,
    },
    {
      prop: 'marginRate',
      name: 'Margin',
      columnType: 'pivotPercent',
      cellProperties: PIVOT_FORMATTING_PRESETS_CELL_PROPERTIES,
    },
    {
      prop: 'orders',
      name: 'Orders',
      columnType: 'pivotInteger',
      cellProperties: PIVOT_FORMATTING_PRESETS_CELL_PROPERTIES,
    },
  ],
  rows: ['region', 'closeDate'],
  columns: ['segment'],
  values: [
    { prop: 'revenue', aggregator: 'sum', label: 'Revenue' },
    { prop: 'marginRate', aggregator: 'avg', label: 'Avg margin' },
    { prop: 'orders', aggregator: 'sum', label: 'Orders' },
  ],
  totals: {
    subtotals: true,
    grandTotal: true,
  },
  groupLabels: {
    null: formatPivotValue(null, { preset: 'date', nullDisplay: 'No close date' }),
  },
  fieldPanel: {
    visible: true,
    allowFieldDragging: true,
    showDataFields: true,
    showRowFields: true,
    showColumnFields: true,
  },
};

export const PIVOT_FORMATTING_PRESETS_PLUGINS = [PivotPlugin, AdvanceFilterPlugin, RowOddPlugin] as any[];

export function getPivotFormattingPresetRows(rows?: DataType[] | null) {
  return Array.isArray(rows) && rows.length > 0 ? rows : PIVOT_FORMATTING_PRESETS_ROWS;
}

export function load(parentSelector: string, rows?: DataType[]) {
  const { isDark } = currentTheme();
  const grid = document.createElement('revo-grid') as HTMLRevoGridElement;
  grid.className = 'grow h-full w-full cell-border';
  grid.range = true;
  grid.resize = true;
  grid.filter = true;
  grid.colSize = 150;
  grid.readonly = true;
  grid.hideAttribution = true;
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.columnTypes = PIVOT_FORMATTING_PRESETS_COLUMN_TYPES;
  grid.plugins = PIVOT_FORMATTING_PRESETS_PLUGINS;
  grid.columns = [];
  Object.assign(grid, {
    pivot: PIVOT_FORMATTING_PRESETS_CONFIG,
  });

  document.querySelector(parentSelector)?.appendChild(grid);

  grid.source = getPivotFormattingPresetRows(rows);

  return () => {
    grid.remove();
  };
}
Vue vue
<template>
  <RevoGrid
    class="grow h-full cell-border"
    hide-attribution
    range
    resize
    filter
    :colSize="150"
    :source="rows"
    :pivot.prop="pivot"
    :theme="isDark ? 'darkCompact' : 'compact'"
    :plugins="plugins"
    :column-types="columnTypes"
    readonly
  />
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import RevoGrid, { type GridPlugin } from '@revolist/vue3-datagrid';
import { currentThemeVue } from '../composables/useRandomData';
import {
  getPivotFormattingPresetRows,
  PIVOT_FORMATTING_PRESETS_COLUMN_TYPES,
  PIVOT_FORMATTING_PRESETS_CONFIG,
  PIVOT_FORMATTING_PRESETS_PLUGINS,
} from './PivotFormattingPresets';

const { isDark } = currentThemeVue();

const props = defineProps<{
  rows?: any[];
}>();

const rows = computed(() => getPivotFormattingPresetRows(props.rows));
const plugins: GridPlugin[] = PIVOT_FORMATTING_PRESETS_PLUGINS;
const columnTypes = ref(PIVOT_FORMATTING_PRESETS_COLUMN_TYPES);

const pivot = computed(() => PIVOT_FORMATTING_PRESETS_CONFIG);
</script>
React tsx
import { useMemo } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { currentTheme } from '../composables/useRandomData';
import {
  getPivotFormattingPresetRows,
  PIVOT_FORMATTING_PRESETS_CELL_PROPERTIES,
  PIVOT_FORMATTING_PRESETS_COLUMN_TYPES,
  PIVOT_FORMATTING_PRESETS_CONFIG,
  PIVOT_FORMATTING_PRESETS_PLUGINS,
} from './PivotFormattingPresets';

interface PivotFormattingPresetsProps {
  rows?: any[];
}

function PivotFormattingPresets({ rows }: PivotFormattingPresetsProps) {
  const { isDark } = currentTheme();
  const data = useMemo(() => getPivotFormattingPresetRows(rows), [rows]);
  const plugins = useMemo(() => PIVOT_FORMATTING_PRESETS_PLUGINS, []);
  const columnTypes = useMemo(() => PIVOT_FORMATTING_PRESETS_COLUMN_TYPES, []);
  const cellProperties = useMemo(() => PIVOT_FORMATTING_PRESETS_CELL_PROPERTIES, []);
  const pivot = useMemo(
    () => ({
      ...PIVOT_FORMATTING_PRESETS_CONFIG,
      dimensions: PIVOT_FORMATTING_PRESETS_CONFIG.dimensions?.map((dimension) => (
        ['revenue', 'marginRate', 'orders'].includes(String(dimension.prop))
          ? { ...dimension, cellProperties }
          : dimension
      )),
    }),
    [cellProperties],
  );

  return (
    <RevoGrid
      className="grow h-full cell-border"
      hideAttribution
      range
      resize
      filter
      colSize={150}
      source={data}
      columns={[]}
      pivot={pivot}
      theme={isDark() ? 'darkCompact' : 'compact'}
      plugins={plugins}
      columnTypes={columnTypes}
      readonly
    />
  );
}

export default PivotFormattingPresets;
Angular ts
import { Component, Input, NO_ERRORS_SCHEMA, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { currentTheme } from '../composables/useRandomData';
import {
  getPivotFormattingPresetRows,
  PIVOT_FORMATTING_PRESETS_COLUMN_TYPES,
  PIVOT_FORMATTING_PRESETS_CONFIG,
  PIVOT_FORMATTING_PRESETS_PLUGINS,
} from './PivotFormattingPresets';

@Component({
  selector: 'pivot-formatting-presets-grid',
  standalone: true,
  imports: [RevoGrid],
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
  template: `
    <revo-grid
      class="grow h-full cell-border"
      style="min-height: 560px"
      [hideAttribution]="true"
      [range]="true"
      [resize]="true"
      [filter]="true"
      [colSize]="150"
      [source]="data"
      [pivot]="pivot"
      [theme]="theme"
      [plugins]="plugins"
      [columnTypes]="columnTypes"
      [readonly]="true"
    ></revo-grid>
  `,
  encapsulation: ViewEncapsulation.None,
})
export class PivotFormattingPresetsGridComponent {
  @Input() set rows(value: any[] | undefined) {
    this.data = getPivotFormattingPresetRows(value);
  }

  data = getPivotFormattingPresetRows();
  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  plugins = PIVOT_FORMATTING_PRESETS_PLUGINS;
  columnTypes = PIVOT_FORMATTING_PRESETS_COLUMN_TYPES;
  pivot = PIVOT_FORMATTING_PRESETS_CONFIG;
}

Import the helpers from Enterprise and create reusable formatters for the value types in your report:

import {
createPivotValueFormatter,
formatPivotValue,
pivotConditionalFormattingPresets,
createPivotConditionalCellProperties,
PivotPlugin,
type PivotConfig,
} from '@revolist/revogrid-enterprise';
const revenueFormatter = createPivotValueFormatter({
preset: 'currency',
currency: 'USD',
locale: 'en-US',
options: { maximumFractionDigits: 0 },
});
const marginFormatter = createPivotValueFormatter({
preset: 'percent',
locale: 'en-US',
options: { minimumFractionDigits: 1, maximumFractionDigits: 1 },
});

createPivotValueFormatter() returns a function. Use it when the same formatting rule is reused by many cells. Use formatPivotValue() when you only need to format one value:

const formattedLabel = formatPivotValue(null, {
preset: 'date',
nullDisplay: 'No close date',
});

Pivot formatters are display helpers. They do not change the original source data or the aggregated number that Pivot calculates. Keep your source values as real numbers and dates, then format them at render time.

const columnTypes = {
pivotCurrency: {
cellTemplate: (_h, props) => revenueFormatter(props.value),
cellProperties: () => ({ class: { 'align-right': true } }),
},
pivotPercent: {
cellTemplate: (_h, props) => marginFormatter(props.value),
cellProperties: () => ({ class: { 'align-right': true } }),
},
};

Attach those column types to the Pivot dimensions that produce analytical value cells:

const pivot: PivotConfig = {
dimensions: [
{ prop: 'region', name: 'Region' },
{ prop: 'closeDate', name: 'Close date', columnType: 'pivotDate' },
{ prop: 'segment', name: 'Segment' },
{ prop: 'revenue', name: 'Revenue', columnType: 'pivotCurrency' },
{ prop: 'marginRate', name: 'Margin', columnType: 'pivotPercent' },
],
rows: ['region', 'closeDate'],
columns: ['segment'],
values: [
{ prop: 'revenue', aggregator: 'sum', label: 'Revenue' },
{ prop: 'marginRate', aggregator: 'avg', label: 'Avg margin' },
],
};

The formatter presets use the browser Intl formatters. Choose the preset that matches the value:

  • number: plain numeric values such as counts, units, or scores.
  • currency: money values. Always provide currency, for example "USD" or "EUR".
  • percent: ratio values such as 0.315, displayed as 31.5%.
  • date: date-only values.
  • datetime: date and time values.
const integerFormatter = createPivotValueFormatter({
preset: 'number',
locale: 'en-US',
options: { maximumFractionDigits: 0 },
});
const eurFormatter = createPivotValueFormatter({
preset: 'currency',
currency: 'EUR',
locale: 'de-DE',
});
const dateFormatter = createPivotValueFormatter({
preset: 'date',
locale: 'en-US',
options: { month: 'short', day: '2-digit', year: 'numeric' },
});

Use pivotConditionalFormattingPresets to describe matching rules, and createPivotConditionalCellProperties() to turn those rules into a RevoGrid cellProperties function.

const conditionalCellProperties = createPivotConditionalCellProperties(
[
pivotConditionalFormattingPresets.gt(300000, {
field: 'revenue',
style: {
backgroundColor: 'rgba(22, 163, 74, 0.16)',
color: '#166534',
fontWeight: '600',
},
}),
pivotConditionalFormattingPresets.lt(0.28, {
field: 'marginRate',
style: {
backgroundColor: 'rgba(220, 38, 38, 0.14)',
color: '#991b1b',
fontWeight: '600',
},
}),
pivotConditionalFormattingPresets.between(0.28, 0.34, {
field: 'marginRate',
style: {
backgroundColor: 'rgba(234, 179, 8, 0.14)',
color: '#854d0e',
},
}),
],
{ match: 'all' },
);

Available presets:

  • gt(value, props): greater than a number.
  • lt(value, props): less than a number.
  • between(min, max, props): between two numbers, including both bounds.
  • equal(value, props): equal to a value.
  • textContains(value, props): contains text.

The optional field narrows a rule to a generated value field. For example, field: 'revenue' applies to generated revenue cells even when Pivot creates column paths such as Software|revenue.

By default, createPivotConditionalCellProperties() only formats Pivot analytical cells. These are generated cells from the values area, not row header fields such as region or closeDate.

This default prevents a rule like gt(300000) from accidentally styling row dimensions that happen to contain comparable values. If you intentionally want rules to apply outside analytical value cells, enable includeNonAnalytical:

const allCellProperties = createPivotConditionalCellProperties(rules, {
includeNonAnalytical: true,
});

Most Pivot reports should keep the default and attach conditional formatting to value dimensions:

const pivot: PivotConfig = {
dimensions: [
{ prop: 'region', name: 'Region' },
{
prop: 'revenue',
name: 'Revenue',
columnType: 'pivotCurrency',
cellProperties: conditionalCellProperties,
},
{
prop: 'marginRate',
name: 'Margin',
columnType: 'pivotPercent',
cellProperties: conditionalCellProperties,
},
],
rows: ['region'],
columns: ['segment'],
values: [
{ prop: 'revenue', aggregator: 'sum' },
{ prop: 'marginRate', aggregator: 'avg' },
],
};

This report groups opportunities by region and close date, splits generated value columns by segment, formats revenue as currency, formats margin as a percent, and highlights high revenue or weak margin.

const rows = [
{ region: 'North America', closeDate: '2026-01-16', segment: 'Software', revenue: 184000, marginRate: 0.37 },
{ region: 'North America', closeDate: '2026-01-22', segment: 'Services', revenue: 126000, marginRate: 0.31 },
{ region: 'EMEA', closeDate: '2026-02-06', segment: 'Services', revenue: 118000, marginRate: 0.28 },
];
const pivot: PivotConfig = {
dimensions: [
{ prop: 'region', name: 'Region' },
{ prop: 'closeDate', name: 'Close date', columnType: 'pivotDate' },
{
prop: 'revenue',
name: 'Revenue',
columnType: 'pivotCurrency',
cellProperties: conditionalCellProperties,
},
{
prop: 'marginRate',
name: 'Margin',
columnType: 'pivotPercent',
cellProperties: conditionalCellProperties,
},
],
rows: ['region', 'closeDate'],
columns: ['segment'],
values: [
{ prop: 'revenue', aggregator: 'sum', label: 'Revenue' },
{ prop: 'marginRate', aggregator: 'avg', label: 'Avg margin' },
],
totals: {
subtotals: true,
grandTotal: true,
},
groupLabels: {
null: formatPivotValue(null, { preset: 'date', nullDisplay: 'No close date' }),
},
};
const grid = document.querySelector('revo-grid');
grid.columnTypes = columnTypes;
grid.plugins = [PivotPlugin];
grid.pivot = pivot;
grid.source = rows;
  • Formatting the source data before Pivot aggregates it. Keep values numeric so sum, avg, and comparisons work correctly.
  • Using preset: 'percent' with whole percentages. 31.5% should usually be stored as 0.315, not 31.5.
  • Forgetting currency when using the currency preset.
  • Expecting conditional formatting to affect row fields by default. It targets analytical value cells unless includeNonAnalytical is enabled.
  • Omitting field on a conditional rule when the report has several value fields. Without field, the rule can match every analytical value cell.
  • Expecting cellTemplate formatting to change conditional comparisons. Conditional rules evaluate the raw cell value, not the formatted display string.

Continue with Sorting And Filtering or Configuration Reference.