Skip to content

Server-Side Pivot

Server-side Pivot is for datasets that are too large, too sensitive, or too analytically expensive to fully materialize on the client. RevoGrid still owns the viewport and interaction model, but the analytical engine owns aggregation, windowing, drilldown facts, and cache-aware execution.

For raw type signatures, see Pivot API. For the full request and store contract, continue with Remote API Reference.

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

import NumberColumnType from '@revolist/revogrid-column-numeral';
import { PivotPlugin } from '@revolist/revogrid-enterprise';
import { PaginationPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { createPivotRemoteStore, PIVOT_REMOTE_CONFIG, PIVOT_REMOTE_FIELDS_VERSION, PIVOT_REMOTE_VIEW_ID } from '../sys-data/pivot.remote';

/**
 * Mounts a live remote-Pivot portal example using the framework-agnostic
 * `HttpPivotRemoteStore` API backed by a mocked local `/api/pivot/*` handler.
 */
export function load(parentSelector: string) {
  const { isDark } = currentTheme();
  const grid = document.createElement('revo-grid');
  grid.className = 'grow h-full w-full cell-border';

  grid.range = true;
  grid.resize = true;
  grid.readonly = true;
  grid.hideAttribution = true;
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.colSize = 180;
  grid.source = [];

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

  grid.plugins = [PivotPlugin, PaginationPlugin, RowOddPlugin];
  Object.assign(grid, {
    pivot: {
      ...PIVOT_REMOTE_CONFIG,
      engine: {
        mode: 'server',
        remoteStore: createPivotRemoteStore(),
        viewId: PIVOT_REMOTE_VIEW_ID,
        fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
        rowAxis: { offset: 0, expandedPaths: [['North']] },
        columnAxis: { offset: 0, limit: 8 },
      },
    },
    pagination: {
      itemsPerPage: 3,
      initialPage: 0,
      total: 0,
    },
  })

  document.querySelector(parentSelector)?.appendChild(grid);
}
Vue vue
<template>
  <RevoGrid
    class="grow h-full cell-border"
    hide-attribution
    range
    resize
    readonly
    :colSize="180"
    :source="[]"
    :pivot.prop="pivot"
    :additionalData="additionalData"
    :theme="isDark ? 'darkCompact' : 'compact'"
    :plugins="plugins"
    :column-types="columnTypes"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import RevoGrid, { type GridPlugin } from '@revolist/vue3-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { PivotPlugin } from '@revolist/revogrid-enterprise';
import { PaginationPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';
import { createPivotRemoteStore, PIVOT_REMOTE_CONFIG, PIVOT_REMOTE_FIELDS_VERSION, PIVOT_REMOTE_VIEW_ID } from '../sys-data/pivot.remote';

const { isDark } = currentThemeVue();

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

const plugins: GridPlugin[] = [PivotPlugin, PaginationPlugin, RowOddPlugin];
const pivot = {
  ...PIVOT_REMOTE_CONFIG,
  engine: {
    mode: 'server' as const,
    remoteStore: createPivotRemoteStore(),
    viewId: PIVOT_REMOTE_VIEW_ID,
    fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
    rowAxis: { offset: 0, expandedPaths: [['North']] },
    columnAxis: { offset: 0, limit: 8 },
  },
};
const additionalData = {
  pagination: {
    itemsPerPage: 3,
    initialPage: 0,
    total: 0,
  },
};
</script>
React tsx
import { useMemo } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { PivotPlugin } from '@revolist/revogrid-enterprise';
import { PaginationPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { createPivotRemoteStore, PIVOT_REMOTE_CONFIG, PIVOT_REMOTE_FIELDS_VERSION, PIVOT_REMOTE_VIEW_ID } from '../sys-data/pivot.remote';

function PivotRemote() {
  const { isDark } = currentTheme();
  const columnTypes = useMemo(
    () => ({
      currency: new NumberColumnType('$0,0.00'),
    }),
    [],
  );
  const plugins = useMemo(() => [PivotPlugin, PaginationPlugin, RowOddPlugin], []);
  const pivot = useMemo(
    () => ({
      ...PIVOT_REMOTE_CONFIG,
      engine: {
        mode: 'server' as const,
        remoteStore: createPivotRemoteStore(),
        viewId: PIVOT_REMOTE_VIEW_ID,
        fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
        rowAxis: { offset: 0, expandedPaths: [['North']] },
        columnAxis: { offset: 0, limit: 8 },
      },
    }),
    [],
  );
  const additionalData = useMemo(
    () => ({
      pagination: {
        itemsPerPage: 3,
        initialPage: 0,
        total: 0,
      },
    }),
    [],
  );

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

export default PivotRemote;
Angular ts
import { Component, ViewEncapsulation, NO_ERRORS_SCHEMA } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { PivotPlugin } from '@revolist/revogrid-enterprise';
import { PaginationPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { createPivotRemoteStore, PIVOT_REMOTE_CONFIG, PIVOT_REMOTE_FIELDS_VERSION, PIVOT_REMOTE_VIEW_ID } from '../sys-data/pivot.remote';

@Component({
  selector: 'pivot-remote-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <revo-grid
      class="grow h-full cell-border"
      [hideAttribution]="true"
      [range]="true"
      [resize]="true"
      [readonly]="true"
      [colSize]="180"
      [source]="[]"
      [pivot]="pivot"
      [pagination]="additionalData.pagination"
      [theme]="theme"
      [plugins]="plugins"
      [columnTypes]="columnTypes"
    ></revo-grid>
  `,
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
})
export class PivotRemoteGridComponent {
  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';

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

  plugins = [PivotPlugin, PaginationPlugin, RowOddPlugin];

  pivot = {
    ...PIVOT_REMOTE_CONFIG,
    engine: {
      mode: 'server' as const,
      remoteStore: createPivotRemoteStore(),
      viewId: PIVOT_REMOTE_VIEW_ID,
      fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
      rowAxis: { offset: 0, expandedPaths: [['North']] },
      columnAxis: { offset: 0, limit: 8 },
    },
  };

  additionalData = {
    pagination: {
      itemsPerPage: 3,
      initialPage: 0,
      total: 0,
    },
  };
}
Mock Remote Store ts
import type { DataType } from '@revolist/revogrid';
import {
  ClientPivotEngineAdapter,
  HttpPivotRemoteStore,
  type PivotConfig,
  type PivotDrilldownCellRef,
  type PivotDrilldownRequest,
  type PivotDrilldownResponse,
  type PivotLoadRequest,
  type PivotLoadResponse,
  type PivotRemoteStore,
  type PivotRemoteStoreHooks,
  type PivotSummaryType,
  type PivotStateResponse,
} from '@revolist/revogrid-enterprise';

/**
 * Sample source rows used by the portal remote Pivot demo. The grid itself only
 * sees the visible analytical block returned by the mocked remote API.
 */
export const PIVOT_REMOTE_ROWS: DataType[] = [
  { region: 'North', rep: 'Jane', year: 2024, quarter: 'Q1', sales: 32, margin: 8 },
  { region: 'North', rep: 'Jane', year: 2024, quarter: 'Q2', sales: 28, margin: 7 },
  { region: 'North', rep: 'John', year: 2024, quarter: 'Q1', sales: 24, margin: 5 },
  { region: 'North', rep: 'John', year: 2024, quarter: 'Q2', sales: 27, margin: 6 },
  { region: 'South', rep: 'Alice', year: 2024, quarter: 'Q1', sales: 22, margin: 4 },
  { region: 'South', rep: 'Alice', year: 2024, quarter: 'Q2', sales: 25, margin: 5 },
  { region: 'South', rep: 'Bob', year: 2024, quarter: 'Q1', sales: 18, margin: 3 },
  { region: 'South', rep: 'Bob', year: 2024, quarter: 'Q2', sales: 20, margin: 4 },
  { region: 'West', rep: 'Mia', year: 2025, quarter: 'Q1', sales: 29, margin: 6 },
  { region: 'West', rep: 'Mia', year: 2025, quarter: 'Q2', sales: 34, margin: 9 },
  { region: 'West', rep: 'Noah', year: 2025, quarter: 'Q1', sales: 21, margin: 4 },
  { region: 'West', rep: 'Noah', year: 2025, quarter: 'Q2', sales: 23, margin: 5 },
];

export const PIVOT_REMOTE_VIEW_ID = 'portal-sales-remote';
export const PIVOT_REMOTE_FIELDS_VERSION = 'portal:remote:v1';

/**
 * Base Pivot configuration shared by all portal remote examples. The remote
 * store is attached by each framework wrapper at runtime.
 */
export const PIVOT_REMOTE_CONFIG: Omit<PivotConfig, 'engine'> = {
  dimensions: [
    { prop: 'region', name: 'Region' },
    { prop: 'rep', name: 'Rep' },
    { prop: 'year', name: 'Year' },
    { prop: 'quarter', name: 'Quarter' },
    { prop: 'sales', name: 'Sales' },
    { prop: 'margin', name: 'Margin' },
  ],
  rows: ['region', 'rep'],
  columns: ['year', 'quarter'],
  values: [{ prop: 'sales', aggregator: 'sum' }],
  filters: ['margin'],
  totals: { grandTotal: true, subtotals: true },
  collapsed: true,
  columnCollapse: { collapsed: true },
  groupAggregations: true,
};

export const PIVOT_REMOTE_SAVED_VIEW_PRESETS: Array<{
  viewId: string;
  name: string;
  description: string;
  config: Omit<PivotConfig, 'engine'>;
}> = [
  {
    viewId: 'north-sales-by-rep',
    name: 'North Sales by Rep',
    description: 'Region and rep rows with quarterly sales columns.',
    config: {
      ...PIVOT_REMOTE_CONFIG,
      rows: ['region', 'rep'],
      columns: ['year', 'quarter'],
      values: [{ prop: 'sales', aggregator: 'sum' }],
      filters: ['margin'],
      expanded: { North: true },
    },
  },
  {
    viewId: 'quarterly-region-sales',
    name: 'Quarterly Region Sales',
    description: 'Region rows with year and quarter sales columns.',
    config: {
      ...PIVOT_REMOTE_CONFIG,
      rows: ['region'],
      columns: ['year', 'quarter'],
      values: [{ prop: 'sales', aggregator: 'sum' }],
      filters: ['rep', 'margin'],
      expanded: {},
    },
  },
  {
    viewId: 'margin-by-region',
    name: 'Margin by Region',
    description: 'Region rows with yearly margin totals.',
    config: {
      ...PIVOT_REMOTE_CONFIG,
      rows: ['region'],
      columns: ['year'],
      values: [{ prop: 'margin', aggregator: 'sum' }],
      filters: ['rep', 'quarter', 'sales'],
      expanded: {},
    },
  },
];

export const PIVOT_REMOTE_DRILLDOWN_CELLS: Array<{
  id: string;
  label: string;
  cell: PivotDrilldownCellRef;
}> = [
  {
    id: 'north-jane-2024-q1',
    label: 'North / Jane / 2024 Q1',
    cell: { rowPath: ['North', 'Jane'], columnPath: [2024, 'Q1'] },
  },
  {
    id: 'south-alice-2024-q2',
    label: 'South / Alice / 2024 Q2',
    cell: { rowPath: ['South', 'Alice'], columnPath: [2024, 'Q2'] },
  },
  {
    id: 'west-mia-2025-q2',
    label: 'West / Mia / 2025 Q2',
    cell: { rowPath: ['West', 'Mia'], columnPath: [2025, 'Q2'] },
  },
];

export const PIVOT_REMOTE_DRILLDOWN_COLUMNS = ['region', 'rep', 'year', 'quarter', 'sales', 'margin'];

export interface PivotRemoteToolsActivity {
  id: string;
  label: string;
  tone: 'neutral' | 'success' | 'warning';
}

type MockStateMap = Map<string, Record<string, unknown>>;

/**
 * Builds a mock fetch implementation that exposes the same `/api/pivot/*`
 * contract used by the real `HttpPivotRemoteStore`.
 */
export function createMockPivotFetch(
  rows: DataType[] = PIVOT_REMOTE_ROWS,
  baseConfig: Omit<PivotConfig, 'engine'> = PIVOT_REMOTE_CONFIG,
) {
  const state = new Map<string, Record<string, unknown>>() as MockStateMap;

  return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
    const url = typeof input === 'string'
      ? new URL(input, 'http://portal.local')
      : new URL(input.toString(), 'http://portal.local');
    const pathname = url.pathname;
    const method = init?.method ?? 'GET';

    if (pathname === '/api/pivot/load' && method === 'POST') {
      const request = JSON.parse(String(init?.body ?? '{}')) as PivotLoadRequest;
      const adapter = new ClientPivotEngineAdapter(createConfigFromLoadRequest(request, baseConfig), rows);
      const response = await adapter.load(request);
      return jsonResponse(response);
    }

    if (pathname === '/api/pivot/drilldown' && method === 'POST') {
      const request = JSON.parse(String(init?.body ?? '{}')) as PivotDrilldownRequest;
      const response = createMockDrilldownResponse(request, rows);
      return jsonResponse(response);
    }

    if (pathname === '/api/pivot/state/save' && method === 'POST') {
      const request = JSON.parse(String(init?.body ?? '{}')) as {
        userId: string;
        viewId: string;
        state: Record<string, unknown>;
      };
      state.set(`${request.userId}:${request.viewId}`, request.state);
      return new Response(null, { status: 204 });
    }

    if (pathname.startsWith('/api/pivot/state/') && method === 'GET') {
      const [, , , userId, viewId] = pathname.split('/');
      const saved = state.get(`${userId}:${viewId}`) ?? { expanded: { North: true } };
      const response: PivotStateResponse = {
        userId,
        viewId,
        state: saved,
        version: 1,
      };
      return jsonResponse(response);
    }

    if (pathname === '/api/pivot/cache/invalidate' && method === 'POST') {
      state.clear();
      return new Response(null, { status: 204 });
    }

    return jsonResponse({ message: 'Not found' }, 404);
  };
}

/**
 * Creates the framework-agnostic remote store used by the live portal demos.
 */
export function createPivotRemoteStore(hooks?: PivotRemoteStoreHooks): PivotRemoteStore {
  return new HttpPivotRemoteStore({
    baseUrl: '',
    tenantId: 'portal-demo',
    datasetWatermark: 'portal-seed-v1',
    authProvider: async () => ({
      Authorization: 'Bearer portal-demo-token',
      'x-tenant-id': 'portal-demo',
    }),
    fetchImpl: createMockPivotFetch() as typeof fetch,
    hooks,
  });
}

export function createPivotRemoteActivityHooks(
  push: (activity: PivotRemoteToolsActivity) => void,
): PivotRemoteStoreHooks {
  let seq = 0;
  const next = (
    label: string,
    tone: PivotRemoteToolsActivity['tone'] = 'neutral',
  ): PivotRemoteToolsActivity => ({
    id: `pivot-remote-activity-${++seq}`,
    label,
    tone,
  });

  return {
    requestStarted: ({ type }) => push(next(`${type}: started`)),
    requestSucceeded: ({ type, cacheStatus }) => {
      push(next(`${type}: succeeded${cacheStatus ? ` (${cacheStatus})` : ''}`, 'success'));
    },
    requestFailed: ({ type }) => push(next(`${type}: failed`, 'warning')),
    cacheStatusChanged: ({ cacheStatus }) => push(next(`cache: ${cacheStatus}`, 'neutral')),
  };
}

export function withPivotRemoteEngine(
  config: Partial<PivotConfig>,
  remoteStore: PivotRemoteStore,
): Partial<PivotConfig> {
  return {
    ...config,
    engine: {
      mode: 'server',
      remoteStore,
      viewId: PIVOT_REMOTE_VIEW_ID,
      fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
      rowAxis: { offset: 0, expandedPaths: [['North']] },
      columnAxis: { offset: 0, limit: 8 },
    },
  };
}

export function withPivotRemoteToolsUi(config: Partial<PivotConfig>): Partial<PivotConfig> {
  return {
    ...config,
    fieldPanel: {
      visible: true,
      allowFieldDragging: true,
      allowFieldRemoving: true,
      showDataFields: true,
      showRowFields: true,
      showColumnFields: true,
      showFilterFields: true,
      ...(typeof config.fieldPanel === 'object' ? config.fieldPanel : {}),
    },
  };
}

export function serializePivotRemoteToolsLayout(config: Partial<PivotConfig>): Partial<PivotConfig> {
  const serializable = { ...config };
  delete serializable.engine;
  delete serializable.mountTo;
  return {
    ...serializable,
    fieldPanel: {
      visible: true,
      allowFieldDragging: true,
      allowFieldRemoving: true,
      showDataFields: true,
      showRowFields: true,
      showColumnFields: true,
      showFilterFields: true,
      ...(typeof serializable.fieldPanel === 'object' ? serializable.fieldPanel : {}),
    },
  };
}

export function createPivotRemoteDiagnosticsRequest(
  config: Partial<PivotConfig> = PIVOT_REMOTE_CONFIG,
): PivotLoadRequest {
  return {
    requestId: `pivot-tools-${Date.now()}`,
    viewId: PIVOT_REMOTE_VIEW_ID,
    fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
    loadOptions: {
      rows: (config.rows ?? []).map((selector) => ({ selector: String(selector) })),
      columns: (config.columns ?? []).map((selector) => ({ selector: String(selector) })),
      totalSummary: (config.values ?? []).map((value) => ({
        selector: String(value.prop),
        summaryType: value.aggregator as PivotSummaryType,
      })),
      groupSummary: (config.values ?? []).map((value) => ({
        selector: String(value.prop),
        summaryType: value.aggregator as PivotSummaryType,
      })),
    },
    viewport: {
      rowAxis: { offset: 0, limit: 6, expandedPaths: [['North']] },
      columnAxis: { offset: 0, limit: 8 },
    },
    uiState: {
      collapsedByDefault: config.collapsed ?? true,
      rowHeaderLayout: 'tree',
    },
  };
}

function createConfigFromLoadRequest(
  request: PivotLoadRequest,
  baseConfig: Omit<PivotConfig, 'engine'>,
): Omit<PivotConfig, 'engine'> {
  const values = request.loadOptions.totalSummary?.length
    ? request.loadOptions.totalSummary.map((summary) => ({
      prop: summary.selector,
      aggregator: summary.summaryType,
    }))
    : baseConfig.values;

  return {
    ...baseConfig,
    rows: request.loadOptions.rows?.map((row) => row.selector) ?? baseConfig.rows,
    columns: request.loadOptions.columns?.map((column) => column.selector) ?? baseConfig.columns,
    values,
    collapsed: request.uiState?.collapsedByDefault ?? baseConfig.collapsed,
  };
}

function createMockDrilldownResponse(
  request: PivotDrilldownRequest,
  rows: DataType[],
): PivotDrilldownResponse {
  const [region, rep] = request.cell.rowPath;
  const [year, quarter] = request.cell.columnPath;
  const filtered = rows.filter((row) => {
    return (region ? row.region === region : true)
      && (rep ? row.rep === rep : true)
      && (year ? row.year === year : true)
      && (quarter ? row.quarter === quarter : true);
  });

  const selectedColumns = request.customColumns?.length
    ? request.customColumns
    : ['region', 'rep', 'year', 'quarter', 'sales'];
  const page = filtered
    .slice(request.offset, request.offset + request.limit)
    .map((row) => Object.fromEntries(selectedColumns.map((column) => [column, row[column]])));

  return {
    requestId: request.requestId,
    data: page,
    totalCount: filtered.length,
    meta: {
      cacheStatus: 'bypass',
    },
  };
}

function jsonResponse(body: unknown, status = 200) {
  return new Response(JSON.stringify(body), {
    status,
    headers: {
      'content-type': 'application/json',
    },
  });
}

The key boundary is:

Pivot UI / RevoGrid Plugin
->
Pivot engine adapter
->
remote store or analytical backend

In practical terms:

  • PivotPlugin receives the direct pivot config
  • pivot.engine.mode = 'server' switches the plugin to remote loading
  • PivotEngineAdapter or PivotRemoteStore handles the analytical request
  • the grid only receives the visible block to render

The core Pivot model does not change:

  • rows
  • columns
  • values
  • totals
  • drill-down related UI state

That is deliberate. The UI should not need one config model for client Pivot and another for server Pivot.

import {
PivotPlugin,
HttpPivotRemoteStore,
type PivotConfig,
} from '@revolist/revogrid-enterprise';
import { PaginationPlugin } from '@revolist/revogrid-pro';
const remoteStore = new HttpPivotRemoteStore({
baseUrl: 'https://example.internal',
tenantId: 'tenant-a',
datasetWatermark: 'sales-v42',
authProvider: async () => ({
Authorization: `Bearer ${await getToken()}`,
}),
});
const pivot: PivotConfig = {
dimensions: [...],
rows: ['Category', 'Subcategory'],
columns: ['Date'],
values: [{ prop: 'SalesAmount', aggregator: 'sum' }],
totals: { grandTotal: true, subtotals: true },
collapsed: true,
engine: {
mode: 'server',
remoteStore,
viewId: 'sales-by-region',
fieldsVersion: 'sha256:fields-v1',
rowAxis: { },
columnAxis: { },
},
};
// PaginationPlugin drives row-axis paging automatically.
// PivotPlugin reads itemsPerPage as rowAxis.limit and writes
// response.rowAxis.totalCount back to pagination.total after each load.
grid.plugins = [PivotPlugin, PaginationPlugin];
grid.pivot = pivot;
grid.additionalData = {
pagination: {
itemsPerPage: 50,
initialPage: 0,
total: 0, // auto-populated from the first server response
},
};

PivotPlugin integrates directly with PaginationPlugin. When both plugins are active:

  • pagination.itemsPerPage becomes the row-axis page size (rowAxis.limit).
  • When the user changes page, PivotPlugin reloads the server window at the new rowAxis.offset.
  • After each successful load, response.rowAxis.totalCount is written back to pagination.total so the pagination panel shows the correct count.
  • Changing the pivot structure (rows, columns, values) resets the offset to 0 automatically.

You do not need to handle page-change events yourself. Drop both plugins into grid.plugins, bind the Pivot config to grid.pivot, and add pagination to additionalData.

What RevoGrid Owns vs What The Engine Owns

Section titled “What RevoGrid Owns vs What The Engine Owns”

RevoGrid and PivotPlugin own:

  • visible grid rendering
  • generated RevoGrid columns
  • visible row block
  • grouped-row metadata used by current drill-down UI
  • cancellation and last-response-wins behavior in the plugin lifecycle

The remote engine or backend owns:

  • selector validation
  • filter execution
  • analytical grouping
  • summary execution
  • row-axis and column-axis windows
  • drilldown fact pages
  • cache policy and invalidation
  1. PivotPlugin sees engine.mode = 'server'.
  2. It builds a PivotLoadRequest from the public Pivot config and current window hints.
  3. The adapter or remote store executes the request.
  4. The response is materialized into generated columns, visible rows, and pinned totals.
  5. RevoGrid renders only the visible analytical window.

Security Boundary

  • Public selectors must be resolved through a field registry.
  • Backend expressions must come from the registry, never from client strings.
  • Query planning belongs behind a planner abstraction so SQL, OLAP, or warehouse backends can share the same request model.

This repository already includes:

  • a field registry model
  • request normalization and limit enforcement
  • a typed error model
  • an SQL-oriented logical planner shape
  • a framework-agnostic HttpPivotRemoteStore

A production Pivot backend needs to translate the analytical request into your engine's native query format. This example demonstrates a TypeScript handler using the core request and response types.

import {
type PivotLoadRequest,
type PivotLoadResponse,
} from '@revolist/revogrid-enterprise';
/**
* Handle POST /api/pivot/load
*/
export async function handlePivotLoad(req: Request): Promise<PivotLoadResponse> {
const request = req.body as PivotLoadRequest;
const { loadOptions, viewport, viewId, fieldsVersion, uiState } = request;
// 1. Authenticate and resolve tenant
const tenantId = await getTenant(req);
// 2. Extract analytical parameters
const {
rows = [], // Group descriptors for rows
columns = [], // Group descriptors for columns
filter, // Complex filter expression tree
sort = [], // Sorting directives
totalSummary = [], // Grand-total measures
groupSummary = [], // Group-level measures (for groupAggregations)
} = loadOptions;
// 3. Extract viewport windows
const { rowAxis, columnAxis } = viewport;
// 4. Execute analytical query (e.g., against SQL or OLAP)
const result = await myAnalyticalEngine.execute({
tenantId,
viewId,
fieldsVersion,
rows,
columns,
filter,
sort,
measures: [...totalSummary, ...groupSummary],
rowWindow: rowAxis,
columnWindow: columnAxis,
// uiState.rowHeaderLayout, uiState.showTotalsPrior, etc.
});
// 5. Build the response
return {
data: result.cells, // REQUIRED: Visible analytical payload
summary: result.totals, // OPTIONAL: Grand-total or pinned rows
rowAxis: {
totalCount: result.rowCount, // REQUIRED: Drives scrollbars/pagination
},
columnAxis: {
paths: result.colPaths, // REQUIRED: Defines the generated headers
},
// Optional: Grouped-row aggregate payloads (if groupSummary requested)
groupAggregates: result.groupSummaries,
meta: {
cacheStatus: result.fromCache ? 'hit' : 'miss', // Used for telemetry
},
};
}

Server-side Pivot is a powerful but complex feature. The standard PivotPlugin implements the core windowing and materialization logic, but some advanced scenarios require custom engine adapters:

  • Implemented: Row-axis windowing, generated columns from paths, tree-mode row headers, grouped aggregations, and grand totals.
  • Custom Adapters: Complex sorting-by-summary or calculated measures typically require a custom PivotEngineAdapter to map your specific backend capabilities to the Pivot contract.
  • Not implemented here: Infinite horizontal scrolling (past the requested column window) or full server-side column grouping collapse (currently managed on client).