Skip to content

Export And State

Pivot export helpers turn the current visible Pivot grid into plain text that users can download, paste, or save. Pivot state JSON saves the Pivot configuration so the same report layout can be restored later.

Source code
TypeScript ts
// src/components/pivot/PivotExportState.ts

import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import type { DataType, GridPlugin } from '@revolist/revogrid';
import {
  buildPivotCopyTsv,
  exportVisiblePivotToCsv,
  parsePivotStateJson,
  PivotPlugin,
  serializePivotStateJson,
  type PivotConfig,
} from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { currentTheme } from '../composables/useRandomData';

const PIVOT_EXPORT_STATE_ROWS: DataType[] = [
  { region: 'North', channel: 'Retail', quarter: 'Q1', revenue: 12400, orders: 48 },
  { region: 'North', channel: 'Retail', quarter: 'Q2', revenue: 16800, orders: 61 },
  { region: 'North', channel: 'Partner', quarter: 'Q1', revenue: 9400, orders: 32 },
  { region: 'South', channel: 'Retail', quarter: 'Q1', revenue: 11200, orders: 45 },
  { region: 'South', channel: 'Partner', quarter: 'Q2', revenue: 15700, orders: 54 },
  { region: 'West', channel: 'Retail', quarter: 'Q2', revenue: 18300, orders: 67 },
];

const PIVOT_EXPORT_STATE_CONFIG: PivotConfig = {
  dimensions: [
    { prop: 'region', name: 'Region' },
    { prop: 'channel', name: 'Channel' },
    { prop: 'quarter', name: 'Quarter' },
    { prop: 'revenue', name: 'Revenue', columnType: 'currency' },
    { prop: 'orders', name: 'Orders', columnType: 'integer' },
  ],
  rows: ['region', 'channel'],
  columns: ['quarter'],
  values: [
    { prop: 'revenue', aggregator: 'sum', label: 'Revenue' },
    { prop: 'orders', aggregator: 'sum', label: 'Orders' },
  ],
  totals: {
    grandTotal: true,
    subtotals: true,
  },
  flatHeaders: false,
};

export function load(parentSelector: string, rows: DataType[] | { isDark?: boolean } = PIVOT_EXPORT_STATE_ROWS) {
  const { isDark } = currentTheme();
  const data = Array.isArray(rows) && rows.length > 0 ? rows : PIVOT_EXPORT_STATE_ROWS;
  let pivotConfig = { ...PIVOT_EXPORT_STATE_CONFIG };

  const container = document.createElement('div');
  container.className = 'grow h-full flex flex-col gap-2';

  const toolbar = document.createElement('div');
  toolbar.className = 'flex flex-wrap gap-2';

  const output = document.createElement('textarea');
  output.className = 'w-full min-h-28 rounded border border-slate-300 bg-transparent p-2 text-xs font-mono';
  output.spellcheck = false;
  output.value = serializePivotStateJson(pivotConfig);

  const status = document.createElement('div');
  status.className = 'text-xs text-slate-500';
  status.textContent = 'Pivot state JSON is ready.';

  const grid = document.createElement('revo-grid') as HTMLRevoGridElement & {
    pivot?: Partial<PivotConfig>;
    pinnedBottomSource?: DataType[];
  };

  grid.className = 'flex-1 min-h-0 w-full cell-border';
  grid.range = true;
  grid.resize = true;
  grid.filter = true;
  grid.colSize = 160;
  grid.readonly = true;
  grid.hideAttribution = true;
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.columnTypes = {
    currency: new NumberColumnType('$0,0.00'),
    integer: new NumberColumnType('0,0'),
  };
  grid.plugins = [PivotPlugin, AdvanceFilterPlugin, RowOddPlugin] as any[] as GridPlugin[];
  grid.columns = [];
  grid.pivot = pivotConfig;

  const readGridModel = () => ({
    columns: grid.columns ?? [],
    source: grid.source ?? [],
    pinnedBottomSource: grid.pinnedBottomSource ?? [],
  });

  const showCsv = () => {
    output.value = exportVisiblePivotToCsv(readGridModel());
    status.textContent = 'CSV generated from the visible Pivot grid.';
  };

  const showTsv = () => {
    output.value = buildPivotCopyTsv(readGridModel());
    status.textContent = 'TSV generated from the visible Pivot grid.';
  };

  const showState = () => {
    output.value = serializePivotStateJson(pivotConfig);
    status.textContent = 'Pivot state JSON generated.';
  };

  const applyState = () => {
    try {
      pivotConfig = parsePivotStateJson(output.value) as PivotConfig;
      grid.pivot = pivotConfig;
      status.textContent = 'Pivot state JSON parsed and applied.';
    } catch (error) {
      status.textContent = error instanceof Error ? error.message : String(error);
    }
  };

  [
    ['CSV', showCsv],
    ['TSV', showTsv],
    ['State JSON', showState],
    ['Apply JSON', applyState],
  ].forEach(([label, onClick]) => {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'rv-btn';
    button.textContent = label as string;
    button.addEventListener('click', onClick as EventListener);
    toolbar.appendChild(button);
  });

  container.appendChild(toolbar);
  container.appendChild(output);
  container.appendChild(status);
  container.appendChild(grid);
  document.querySelector(parentSelector)?.appendChild(container);

  grid.source = data;

  return () => {
    container.remove();
  };
}
Vue vue
<template>
  <div class="grow h-full flex flex-col gap-2">
    <div class="flex flex-wrap gap-2">
      <button class="rv-btn" type="button" @click="showCsv">CSV</button>
      <button class="rv-btn" type="button" @click="showTsv">TSV</button>
      <button class="rv-btn" type="button" @click="showState">State JSON</button>
      <button class="rv-btn" type="button" @click="applyState">Apply JSON</button>
    </div>
    <textarea
      v-model="output"
      class="w-full min-h-28 rounded border border-slate-300 bg-transparent p-2 text-xs font-mono"
      spellcheck="false"
    />
    <div class="text-xs text-slate-500">{{ status }}</div>
    <RevoGrid
      ref="gridRef"
      class="flex-1 min-h-0 cell-border"
      hide-attribution
      range
      resize
      filter
      :colSize="160"
      :source="rows"
      :pivot.prop="pivot"
      :theme="isDark ? 'darkCompact' : 'compact'"
      :plugins="plugins"
      :column-types="columnTypes"
      readonly
    />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import { currentThemeVue } from '../composables/useRandomData';
import RevoGrid from '@revolist/vue3-datagrid';
import type { GridPlugin } from '@revolist/revogrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
  buildPivotCopyTsv,
  exportVisiblePivotToCsv,
  parsePivotStateJson,
  PivotPlugin,
  serializePivotStateJson,
  type PivotConfig,
} from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, RowOddPlugin } from '@revolist/revogrid-pro';

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

const PIVOT_EXPORT_STATE_ROWS = [
  { region: 'North', channel: 'Retail', quarter: 'Q1', revenue: 12400, orders: 48 },
  { region: 'North', channel: 'Retail', quarter: 'Q2', revenue: 16800, orders: 61 },
  { region: 'North', channel: 'Partner', quarter: 'Q1', revenue: 9400, orders: 32 },
  { region: 'South', channel: 'Retail', quarter: 'Q1', revenue: 11200, orders: 45 },
  { region: 'South', channel: 'Partner', quarter: 'Q2', revenue: 15700, orders: 54 },
  { region: 'West', channel: 'Retail', quarter: 'Q2', revenue: 18300, orders: 67 },
];

const PIVOT_EXPORT_STATE_CONFIG: PivotConfig = {
  dimensions: [
    { prop: 'region', name: 'Region' },
    { prop: 'channel', name: 'Channel' },
    { prop: 'quarter', name: 'Quarter' },
    { prop: 'revenue', name: 'Revenue', columnType: 'currency' },
    { prop: 'orders', name: 'Orders', columnType: 'integer' },
  ],
  rows: ['region', 'channel'],
  columns: ['quarter'],
  values: [
    { prop: 'revenue', aggregator: 'sum', label: 'Revenue' },
    { prop: 'orders', aggregator: 'sum', label: 'Orders' },
  ],
  totals: {
    grandTotal: true,
    subtotals: true,
  },
  flatHeaders: false,
};

const rows = ref(Array.isArray(props.rows) && props.rows.length > 0 ? props.rows : PIVOT_EXPORT_STATE_ROWS);
const gridRef = ref<(InstanceType<typeof RevoGrid> & { $el?: HTMLRevoGridElement }) | HTMLRevoGridElement | null>(null);
const pivotConfig = ref<PivotConfig>({ ...PIVOT_EXPORT_STATE_CONFIG });
const output = ref(serializePivotStateJson(pivotConfig.value));
const status = ref('Pivot state JSON is ready.');

const columnTypes = ref({
  currency: new NumberColumnType('$0,0.00'),
  integer: new NumberColumnType('0,0'),
});

const plugins: GridPlugin[] = [PivotPlugin, AdvanceFilterPlugin, RowOddPlugin] as any[];
const pivot = computed(() => pivotConfig.value);

function getGrid() {
  const refValue = gridRef.value as (InstanceType<typeof RevoGrid> & { $el?: HTMLRevoGridElement }) | HTMLRevoGridElement | null;
  return (refValue && '$el' in refValue ? refValue.$el : refValue) as (HTMLRevoGridElement & { pinnedBottomSource?: any[] }) | null;
}

function readGridModel() {
  const grid = getGrid();
  return {
    columns: grid?.columns ?? [],
    source: grid?.source ?? [],
    pinnedBottomSource: grid?.pinnedBottomSource ?? [],
  };
}

function showCsv() {
  output.value = exportVisiblePivotToCsv(readGridModel());
  status.value = 'CSV generated from the visible Pivot grid.';
}

function showTsv() {
  output.value = buildPivotCopyTsv(readGridModel());
  status.value = 'TSV generated from the visible Pivot grid.';
}

function showState() {
  output.value = serializePivotStateJson(pivotConfig.value);
  status.value = 'Pivot state JSON generated.';
}

function applyState() {
  try {
    pivotConfig.value = parsePivotStateJson(output.value) as PivotConfig;
    status.value = 'Pivot state JSON parsed and applied.';
  } catch (error) {
    status.value = error instanceof Error ? error.message : String(error);
  }
}
</script>
React tsx
// src/components/pivot/PivotExportState.tsx

import { useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import type { GridPlugin } from '@revolist/revogrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
  buildPivotCopyTsv,
  exportVisiblePivotToCsv,
  parsePivotStateJson,
  PivotPlugin,
  serializePivotStateJson,
  type PivotConfig,
} from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';

interface PivotExportStateProps {
  rows?: any[];
}

const PIVOT_EXPORT_STATE_ROWS = [
  { region: 'North', channel: 'Retail', quarter: 'Q1', revenue: 12400, orders: 48 },
  { region: 'North', channel: 'Retail', quarter: 'Q2', revenue: 16800, orders: 61 },
  { region: 'North', channel: 'Partner', quarter: 'Q1', revenue: 9400, orders: 32 },
  { region: 'South', channel: 'Retail', quarter: 'Q1', revenue: 11200, orders: 45 },
  { region: 'South', channel: 'Partner', quarter: 'Q2', revenue: 15700, orders: 54 },
  { region: 'West', channel: 'Retail', quarter: 'Q2', revenue: 18300, orders: 67 },
];

const PIVOT_EXPORT_STATE_CONFIG: PivotConfig = {
  dimensions: [
    { prop: 'region', name: 'Region' },
    { prop: 'channel', name: 'Channel' },
    { prop: 'quarter', name: 'Quarter' },
    { prop: 'revenue', name: 'Revenue', columnType: 'currency' },
    { prop: 'orders', name: 'Orders', columnType: 'integer' },
  ],
  rows: ['region', 'channel'],
  columns: ['quarter'],
  values: [
    { prop: 'revenue', aggregator: 'sum', label: 'Revenue' },
    { prop: 'orders', aggregator: 'sum', label: 'Orders' },
  ],
  totals: {
    grandTotal: true,
    subtotals: true,
  },
  flatHeaders: false,
};

function PivotExportState({ rows }: PivotExportStateProps) {
  const { isDark } = currentTheme();
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const data = useMemo(() => (Array.isArray(rows) && rows.length > 0 ? rows : PIVOT_EXPORT_STATE_ROWS), [rows]);
  const [pivotConfig, setPivotConfig] = useState<PivotConfig>(PIVOT_EXPORT_STATE_CONFIG);
  const [output, setOutput] = useState(() => serializePivotStateJson(PIVOT_EXPORT_STATE_CONFIG));
  const [status, setStatus] = useState('Pivot state JSON is ready.');

  const columnTypes = useMemo(
    () => ({
      currency: new NumberColumnType('$0,0.00'),
      integer: new NumberColumnType('0,0'),
    }),
    [],
  );

  const plugins = useMemo(() => [PivotPlugin, AdvanceFilterPlugin, RowOddPlugin] as any[] as GridPlugin[], []);
  const pivot = useMemo(() => pivotConfig, [pivotConfig]);

  const readGridModel = () => {
    const grid = gridRef.current as (HTMLRevoGridElement & { pinnedBottomSource?: any[] }) | null;
    return {
      columns: grid?.columns ?? [],
      source: grid?.source ?? [],
      pinnedBottomSource: grid?.pinnedBottomSource ?? [],
    };
  };

  const showCsv = () => {
    setOutput(exportVisiblePivotToCsv(readGridModel()));
    setStatus('CSV generated from the visible Pivot grid.');
  };

  const showTsv = () => {
    setOutput(buildPivotCopyTsv(readGridModel()));
    setStatus('TSV generated from the visible Pivot grid.');
  };

  const showState = () => {
    setOutput(serializePivotStateJson(pivotConfig));
    setStatus('Pivot state JSON generated.');
  };

  const applyState = () => {
    try {
      const nextConfig = parsePivotStateJson(output) as PivotConfig;
      setPivotConfig(nextConfig);
      setStatus('Pivot state JSON parsed and applied.');
    } catch (error) {
      setStatus(error instanceof Error ? error.message : String(error));
    }
  };

  return (
    <div className="grow h-full flex flex-col gap-2">
      <div className="flex flex-wrap gap-2">
        <button className="rv-btn" type="button" onClick={showCsv}>CSV</button>
        <button className="rv-btn" type="button" onClick={showTsv}>TSV</button>
        <button className="rv-btn" type="button" onClick={showState}>State JSON</button>
        <button className="rv-btn" type="button" onClick={applyState}>Apply JSON</button>
      </div>
      <textarea
        className="w-full min-h-28 rounded border border-slate-300 bg-transparent p-2 text-xs font-mono"
        value={output}
        spellCheck={false}
        onChange={event => setOutput(event.target.value)}
      />
      <div className="text-xs text-slate-500">{status}</div>
      <RevoGrid
        ref={gridRef}
        className="flex-1 min-h-0 cell-border"
        hideAttribution
        range
        resize
        filter
        colSize={160}
        source={data}
        columns={[]}
        pivot={pivot}
        theme={isDark() ? 'darkCompact' : 'compact'}
        plugins={plugins}
        columnTypes={columnTypes}
        readonly
      />
    </div>
  );
}

export default PivotExportState;
Angular ts
import { Component, ElementRef, NO_ERRORS_SCHEMA, ViewChild, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RevoGrid } from '@revolist/angular-datagrid';
import type { GridPlugin } from '@revolist/revogrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
  buildPivotCopyTsv,
  exportVisiblePivotToCsv,
  parsePivotStateJson,
  PivotPlugin,
  serializePivotStateJson,
  type PivotConfig,
} from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';

const PIVOT_EXPORT_STATE_ROWS = [
  { region: 'North', channel: 'Retail', quarter: 'Q1', revenue: 12400, orders: 48 },
  { region: 'North', channel: 'Retail', quarter: 'Q2', revenue: 16800, orders: 61 },
  { region: 'North', channel: 'Partner', quarter: 'Q1', revenue: 9400, orders: 32 },
  { region: 'South', channel: 'Retail', quarter: 'Q1', revenue: 11200, orders: 45 },
  { region: 'South', channel: 'Partner', quarter: 'Q2', revenue: 15700, orders: 54 },
  { region: 'West', channel: 'Retail', quarter: 'Q2', revenue: 18300, orders: 67 },
];

const PIVOT_EXPORT_STATE_CONFIG: PivotConfig = {
  dimensions: [
    { prop: 'region', name: 'Region' },
    { prop: 'channel', name: 'Channel' },
    { prop: 'quarter', name: 'Quarter' },
    { prop: 'revenue', name: 'Revenue', columnType: 'currency' },
    { prop: 'orders', name: 'Orders', columnType: 'integer' },
  ],
  rows: ['region', 'channel'],
  columns: ['quarter'],
  values: [
    { prop: 'revenue', aggregator: 'sum', label: 'Revenue' },
    { prop: 'orders', aggregator: 'sum', label: 'Orders' },
  ],
  totals: {
    grandTotal: true,
    subtotals: true,
  },
  flatHeaders: false,
};

@Component({
  selector: 'pivot-export-state-grid',
  standalone: true,
  imports: [RevoGrid, FormsModule],
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
  template: `
    <div class="grow h-full flex flex-col gap-2" style="min-height: 640px">
      <div class="flex flex-wrap gap-2">
        <button class="rv-btn" type="button" (click)="showCsv()">CSV</button>
        <button class="rv-btn" type="button" (click)="showTsv()">TSV</button>
        <button class="rv-btn" type="button" (click)="showState()">State JSON</button>
        <button class="rv-btn" type="button" (click)="applyState()">Apply JSON</button>
      </div>
      <textarea
        class="w-full min-h-28 rounded border border-slate-300 bg-transparent p-2 text-xs font-mono"
        spellcheck="false"
        [(ngModel)]="output"
      ></textarea>
      <div class="text-xs text-slate-500">{{ status }}</div>
      <revo-grid
        #grid
        class="flex-1 min-h-0 cell-border"
        style="min-height: 430px"
        [hideAttribution]="true"
        [range]="true"
        [resize]="true"
        [filter]="true"
        [colSize]="160"
        [source]="rows"
        [pivot]="pivot"
        [theme]="theme"
        [plugins]="plugins"
        [columnTypes]="columnTypes"
        [readonly]="true"
      ></revo-grid>
    </div>
  `,
  encapsulation: ViewEncapsulation.None,
})
export class PivotExportStateGridComponent {
  @ViewChild('grid', { read: ElementRef }) gridRef!: ElementRef<HTMLRevoGridElement & { pinnedBottomSource?: any[] }>;

  rows = PIVOT_EXPORT_STATE_ROWS;

  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';

  columnTypes = {
    currency: new NumberColumnType('$0,0.00'),
    integer: new NumberColumnType('0,0'),
  };

  plugins: GridPlugin[] = [PivotPlugin, AdvanceFilterPlugin, RowOddPlugin] as any[];

  pivotConfig = { ...PIVOT_EXPORT_STATE_CONFIG };

  pivot = this.pivotConfig;

  output = serializePivotStateJson(this.pivotConfig);

  status = 'Pivot state JSON is ready.';

  showCsv() {
    this.output = exportVisiblePivotToCsv(this.readGridModel());
    this.status = 'CSV generated from the visible Pivot grid.';
  }

  showTsv() {
    this.output = buildPivotCopyTsv(this.readGridModel());
    this.status = 'TSV generated from the visible Pivot grid.';
  }

  showState() {
    this.output = serializePivotStateJson(this.pivotConfig);
    this.status = 'Pivot state JSON generated.';
  }

  applyState() {
    try {
      this.pivotConfig = parsePivotStateJson(this.output) as PivotConfig;
      this.pivot = this.pivotConfig;
      this.status = 'Pivot state JSON parsed and applied.';
    } catch (error) {
      this.status = error instanceof Error ? error.message : String(error);
    }
  }

  private readGridModel() {
    const grid = this.gridRef?.nativeElement;
    return {
      columns: grid?.columns ?? [],
      source: grid?.source ?? [],
      pinnedBottomSource: grid?.pinnedBottomSource ?? [],
    };
  }
}

CSV means comma-separated values. Use CSV when users need a file they can open in Excel, Numbers, Google Sheets, or another reporting tool. The Pivot CSV helper flattens grouped column headers into readable header paths and appends pinned bottom rows, such as grand totals, after the body rows.

TSV means tab-separated values. Use TSV for copy and paste because spreadsheets understand tabs as cell boundaries. The Pivot TSV helper keeps row-axis headers and column-axis headers separated, which makes pasted Pivot output easier to read.

State JSON is not table data. It is a saved Pivot configuration, including fields such as rows, columns, values, filters, totals, collapsed state, and expanded groups. Use state JSON when users need to save a report layout, share a layout, or restore a layout after reloading the page.

Import the helpers from Enterprise with the Pivot plugin:

import {
buildPivotCopyTsv,
exportVisiblePivotToCsv,
parsePivotStateJson,
PivotPlugin,
serializePivotStateJson,
type PivotConfig,
} from '@revolist/revogrid-enterprise';

Render Pivot as usual:

const pivot: PivotConfig = {
rows: ['region', 'channel'],
columns: ['quarter'],
values: [
{ prop: 'revenue', aggregator: 'sum', label: 'Revenue' },
{ prop: 'orders', aggregator: 'sum', label: 'Orders' },
],
totals: {
grandTotal: true,
subtotals: true,
},
};
grid.plugins = [PivotPlugin];
grid.pivot = pivot;
grid.source = rows;

CSV and TSV helpers work from the current grid model, not from the original raw dataset. Pass the visible columns, visible source, and any pinnedBottomSource.

function readGridModel(grid: HTMLRevoGridElement & { pinnedBottomSource?: any[] }) {
return {
columns: grid.columns ?? [],
source: grid.source ?? [],
pinnedBottomSource: grid.pinnedBottomSource ?? [],
};
}

This matters because Pivot generates columns and rows from the source data. Exporting the original array would miss generated value columns, grouped headers, subtotal rows, and grand-total rows.

Use exportVisiblePivotToCsv when the user wants a file-oriented export:

const csv = exportVisiblePivotToCsv(readGridModel(grid));

For browser download:

const csv = exportVisiblePivotToCsv(readGridModel(grid));
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'pivot-report.csv';
link.click();
URL.revokeObjectURL(url);

CSV is best for file export, email attachments, imports into other systems, and audit snapshots.

Use buildPivotCopyTsv when the user wants clipboard text:

const tsv = buildPivotCopyTsv(readGridModel(grid));
await navigator.clipboard.writeText(tsv);

TSV is best for copy actions because pasting into a spreadsheet preserves cells without requiring the user to import a file.

You can also limit copy output when you know the selected props or selected rows:

const tsv = buildPivotCopyTsv({
...readGridModel(grid),
selectedProps: ['region', '__pivot_grand_total__|revenue'],
selectedRowIndexes: [0, 1, 2],
});

Only pass selected props that exist in the current visible columns. Missing props are ignored.

Use serializePivotStateJson to turn the current Pivot configuration into deterministic JSON:

const json = serializePivotStateJson(pivot);
localStorage.setItem('sales:pivot-state', json);

The JSON is wrapped with a kind and version so the parser can reject unsupported state:

{
"config": {
"columns": ["quarter"],
"rows": ["region", "channel"],
"values": [{ "aggregator": "sum", "prop": "revenue" }]
},
"kind": "revogrid:pivot.state",
"version": 1
}

Use parsePivotStateJson to validate and restore it:

try {
const saved = localStorage.getItem('sales:pivot-state');
if (saved) {
grid.pivot = parsePivotStateJson(saved) as PivotConfig;
}
} catch (error) {
console.error('Could not restore Pivot state:', error);
}

State JSON is best for saved views, shared report layouts, user preferences, and restoring drill-down state after refresh.

This example exposes four buttons:

  • CSV: reads the visible Pivot grid and writes CSV text into the output box.
  • TSV: reads the visible Pivot grid and writes clipboard-ready TSV into the output box.
  • State JSON: serializes the current Pivot configuration.
  • Apply JSON: parses the edited JSON and applies it back to the grid.

The important part is keeping the data export path separate from the state path:

let pivotConfig: PivotConfig = {
rows: ['region', 'channel'],
columns: ['quarter'],
values: [
{ prop: 'revenue', aggregator: 'sum', label: 'Revenue' },
{ prop: 'orders', aggregator: 'sum', label: 'Orders' },
],
};
function showCsv() {
output.value = exportVisiblePivotToCsv(readGridModel(grid));
}
function showTsv() {
output.value = buildPivotCopyTsv(readGridModel(grid));
}
function showState() {
output.value = serializePivotStateJson(pivotConfig);
}
function applyState() {
pivotConfig = parsePivotStateJson(output.value) as PivotConfig;
grid.pivot = pivotConfig;
}
  • Exporting the original source rows instead of the visible grid model. Use grid.columns, grid.source, and grid.pinnedBottomSource.
  • Using CSV for copy and paste. TSV usually pastes into spreadsheet cells more reliably.
  • Expecting state JSON to contain row data. It stores Pivot configuration, not the source dataset or exported result.
  • Applying edited JSON without try / catch. Invalid JSON, unsupported versions, and invalid field shapes throw errors.
  • Forgetting pinned bottom rows. If grand totals are enabled, include pinnedBottomSource so totals appear in CSV and TSV output.
  • Saving functions in state JSON. State JSON can only contain JSON-safe configuration values; custom runtime callbacks must be restored by application code.

Continue with Sorting And Filtering or Field Panel.