Skip to content

Financial Pivot Showcase

This showcase uses 3,600 original synthetic records shaped like a familiar financial sales model. The schema is inspired by Microsoft’s Financial Sample workbook, but the bundled values and product names are generated specifically for this demo.

Source code
TypeScriptts
/** Vanilla host for the shared financial report model. Vue/React/Angular use the same data, presets, actions and state helpers. */
import { defineCustomElements } from '@revolist/revogrid/loader';
import type { PivotConfig, PivotChartRef } from '@revolist/pivot';
import './financial-pivot-header/financial-pivot-header.scss';
import { currentTheme, observeCurrentTheme } from './shared/theme';
import { loadContosoData } from './contoso-data';
import {
  hasSavedPivotReport,
  handleReportAction,
  listSavedPivotReports,
  loadSavedPivotReport,
  persistPivotState,
  restoreActivePreset,
  restorePivotConfig,
} from './financial.analytics';
import { exportFinancialCsv, exportFinancialXlsx, financialActionError, mountFinancialChart, printFinancialPivot } from './financial.actions';
import { createFinancialFieldsDrawer } from './financial-fields-drawer';
import {
  FINANCIAL_COLUMNS,
  FINANCIAL_COLUMN_TYPES,
  FINANCIAL_MULTI_ROW_HEADER,
  FINANCIAL_PIVOT_CHARTS,
  FINANCIAL_PIVOT_CHARTS_UI,
  FINANCIAL_SHOWCASE_PLUGINS,
  applyFinancialPivotOptions,
  createFinancialPreset,
  fitFinancialPivotColumns,
  resolveFinancialRows,
  type FinancialPresetId,
} from './financial.pivot';
import {
  FINANCIAL_PIVOT_CONFIGURATOR_EVENT,
  FINANCIAL_PIVOT_EXPANDED_EVENT,
  FINANCIAL_PIVOT_ACTION_EVENT,
  FINANCIAL_PIVOT_PRESET_EVENT,
  createFinancialPivotHeader,
  type FinancialPivotAction,
} from './financial-pivot-header/financial-pivot-header';

defineCustomElements();

export function load(
  parentSelector: string,
  rows: any[] | { isDark?: boolean } = [],
  options: { initialPreset?: FinancialPresetId; chartTitle?: string } = {},
) {
  const parent = document.querySelector(parentSelector);
  if (!parent) return;

  const { isDark } = currentTheme();
  const providedRows = Array.isArray(rows) && rows.length ? rows : undefined;
  let data = resolveFinancialRows(providedRows);
  let disposed = false;
  let activePreset: FinancialPresetId = options.initialPreset ?? restoreActivePreset();
  let pivotConfig: PivotConfig = restorePivotConfig(activePreset);
  let configuratorVisible = false;
  let expanded = false;
  let chartVisible = false;
  let chart: PivotChartRef | undefined;
  let savedAvailable = hasSavedPivotReport();

  const container = document.createElement('div');
  container.className = 'financial-pivot-showcase grow flex flex-col gap-2 h-full p-2 box-border';

  const header = createFinancialPivotHeader({
    activePreset,
    configuratorVisible,
    expanded,
    chartVisible,
    savedAvailable,
    savedReports: listSavedPivotReports(),
  });

  const summaryMount = document.createElement('div');

  const chartContainer = document.createElement('div');
  chartContainer.className = 'financial-pivot-chart';
  chartContainer.setAttribute('aria-label', 'Pivot chart');
  chartContainer.hidden = true;
  const actionError = document.createElement('div');
  actionError.setAttribute('role', 'alert');
  actionError.className = 'financial-pivot-header__question';

  const scrollContainer = document.createElement('div');
  scrollContainer.className = 'financial-pivot-grid-view grow min-h-0 overflow-auto';
  const gridContainer = document.createElement('div');
  gridContainer.className = 'pivot-grid-container h-full overflow-hidden';
  let grid: HTMLRevoGridElement;
  const fieldsDrawer = createFinancialFieldsDrawer(() => {
    configuratorVisible = false;
    applyPivotOptions();
    refreshLayout();
  });

  function applyPivotOptions() {
    fitFinancialPivotColumns(grid, gridContainer, pivotConfig);
    grid.pivot = applyFinancialPivotOptions(
      pivotConfig,
      data,
      configuratorVisible,
      fieldsDrawer.mountTo,
      gridContainer.clientWidth,
    );
  }

  function refreshLayout() {
    header.state = {
      activePreset,
      configuratorVisible,
      expanded,
      chartVisible,
      savedAvailable,
      savedReports: listSavedPivotReports(),
    };
    chartContainer.hidden = !chartVisible;
    container.classList.toggle('financial-pivot-showcase--chart', chartVisible);
    scrollContainer.setAttribute('aria-hidden', String(chartVisible));
    container.dataset.expanded = String(expanded);
    if (grid) fitFinancialPivotColumns(grid, gridContainer, pivotConfig);
  }

  const onPivotConfigUpdate = (event: CustomEvent<PivotConfig>) => {
    pivotConfig = event.detail || createFinancialPreset();
    refreshLayout();
  };
  function initializeGrid() {
    grid = document.createElement('revo-grid') as any;
    grid.className = 'overflow-hidden skip-style h-full min-h-0 cell-border';
    Object.assign(grid, {
      hideAttribution: true,
      range: true,
      resize: true,
      filter: true,
      multiRowHeader: FINANCIAL_MULTI_ROW_HEADER,
      colSize: 128,
      readonly: true,
      theme: isDark() ? 'darkCompact' : 'compact',
      columns: FINANCIAL_COLUMNS,
      columnTypes: FINANCIAL_COLUMN_TYPES,
      plugins: FINANCIAL_SHOWCASE_PLUGINS,
      pivotSummaryPanel: { mountTo: summaryMount },
      pivotCharts: FINANCIAL_PIVOT_CHARTS,
      pivotChartsUi: FINANCIAL_PIVOT_CHARTS_UI,
      pivot: applyFinancialPivotOptions(
        pivotConfig,
        data,
        configuratorVisible,
        fieldsDrawer.mountTo,
        gridContainer.clientWidth,
      ),
    });
    grid.addEventListener('pivot-config-update', onPivotConfigUpdate as EventListener);
    gridContainer.appendChild(grid);
    grid.source = data;
  }

  function replacePivotOptions() {
    grid.pivot = undefined;
    window.setTimeout(applyPivotOptions);
  }

  header.addEventListener(FINANCIAL_PIVOT_PRESET_EVENT, (event) => {
    const id = (event as CustomEvent<FinancialPresetId>).detail;
    pivotConfig = createFinancialPreset(id);
    activePreset = id;
    refreshLayout();
    replacePivotOptions();
  });
  header.addEventListener(FINANCIAL_PIVOT_CONFIGURATOR_EVENT, () => {
    if (configuratorVisible) fieldsDrawer.hide();
    else {
      fieldsDrawer.show(document.activeElement as HTMLElement);
      configuratorVisible = true;
    }
    applyPivotOptions();
    refreshLayout();
  });
  header.addEventListener(FINANCIAL_PIVOT_EXPANDED_EVENT, () => {
    expanded = !expanded;
    refreshLayout();
  });
  header.addEventListener(FINANCIAL_PIVOT_ACTION_EVENT, async (event) => {
    const action = (event as CustomEvent<FinancialPivotAction>).detail;
    actionError.textContent = '';
    try {
      if (action === 'export-json' || typeof action === 'object') {
        const result = handleReportAction(action, pivotConfig, activePreset);
        if (result.restored) {
          pivotConfig = result.restored.config;
          activePreset = result.restored.preset;
          replacePivotOptions();
        }
        refreshLayout();
        actionError.textContent = result.error ?? '';
        return;
      }
      if (action === 'reset') {
        activePreset = options.initialPreset ?? 'sales';
        pivotConfig = createFinancialPreset(activePreset);
        refreshLayout();
        replacePivotOptions();
      }
      if (action === 'save') {
        savedAvailable = persistPivotState(pivotConfig, activePreset);
        if (!savedAvailable) actionError.textContent = 'This browser could not save the report.';
        refreshLayout();
      }
      if (action === 'restore') {
        const saved = loadSavedPivotReport();
        if (saved) {
          pivotConfig = saved.config;
          activePreset = saved.preset;
          refreshLayout();
          replacePivotOptions();
        } else actionError.textContent = 'The saved report could not be restored.';
      }
      if (action === 'export') await exportFinancialCsv(grid);
      if (action === 'export-xlsx') await exportFinancialXlsx(grid);
      if (action === 'print') await printFinancialPivot(grid);
      if (action === 'chart') {
        chartVisible = !chartVisible;
        refreshLayout();
        if (chartVisible && !chart) chart = await mountFinancialChart(grid, chartContainer, options.chartTitle);
      }
    } catch (error) {
      if (action === 'chart') { chartVisible = false; refreshLayout(); }
      actionError.textContent = financialActionError(error);
    }
  });
  scrollContainer.appendChild(gridContainer);
  container.append(header, summaryMount, actionError, chartContainer, scrollContainer);
  parent.appendChild(container);
  refreshLayout();
  initializeGrid();
  let narrowHost = gridContainer.clientWidth < 650;
  const resizeObserver = new ResizeObserver(() => {
    fitFinancialPivotColumns(grid, gridContainer, pivotConfig);
    const narrow = gridContainer.clientWidth < 650;
    if (narrow !== narrowHost) {
      narrowHost = narrow;
      applyPivotOptions();
    }
  });
  resizeObserver.observe(gridContainer);
  if (!providedRows) {
    actionError.textContent = 'Loading Contoso retail data…';
    void loadContosoData().then((dataset) => {
      if (disposed) return;
      data = dataset.rows;
      grid.source = data;
      applyPivotOptions();
      actionError.textContent = '';
      refreshLayout();
    }).catch((error) => {
      if (!disposed) actionError.textContent = `Could not load the Contoso dataset: ${error instanceof Error ? error.message : String(error)}`;
    });
  }
  const disconnectTheme = observeCurrentTheme((darkTheme) => {
    grid.theme = darkTheme ? 'darkCompact' : 'compact';
  });

  return () => {
    disposed = true;
    disconnectTheme();
    resizeObserver.disconnect();
    chart?.destroy();
    fieldsDrawer.destroy();
    grid.removeEventListener('pivot-config-update', onPivotConfigUpdate as EventListener);
    grid.remove();
    container.remove();
  };
}
Reacttsx
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type DetailedHTMLProps,
  type HTMLAttributes,
} from 'react';
import { RevoGrid, type DataType } from '@revolist/react-datagrid';
import type { PivotConfig, PivotChartRef } from '@revolist/pivot';
import './financial-pivot-header/financial-pivot-header.scss';
import { currentTheme, observeCurrentTheme } from './shared/theme';
import { loadContosoData } from './contoso-data';
import {
  hasSavedPivotReport,
  handleReportAction,
  listSavedPivotReports,
  loadSavedPivotReport,
  persistPivotState,
  restorePivotConfig,
} from './financial.analytics';
import { exportFinancialCsv, exportFinancialXlsx, financialActionError, mountFinancialChart, printFinancialPivot } from './financial.actions';
import { createFinancialFieldsDrawer, type FinancialFieldsDrawer } from './financial-fields-drawer';
import {
  FINANCIAL_COLUMNS,
  FINANCIAL_COLUMN_TYPES,
  FINANCIAL_MULTI_ROW_HEADER,
  FINANCIAL_PIVOT_CHARTS,
  FINANCIAL_PIVOT_CHARTS_UI,
  FINANCIAL_SHOWCASE_PLUGINS,
  applyFinancialPivotOptions,
  createFinancialPreset,
  financialPivotColumnSize,
  resolveFinancialRows,
  type FinancialPresetId,
} from './financial.pivot';
import {
  FINANCIAL_PIVOT_CONFIGURATOR_EVENT,
  FINANCIAL_PIVOT_EXPANDED_EVENT,
  FINANCIAL_PIVOT_ACTION_EVENT,
  FINANCIAL_PIVOT_PRESET_EVENT,
  defineFinancialPivotHeaderElement,
  type FinancialPivotHeaderElement,
  type FinancialPivotHeaderState,
  type FinancialPivotAction,
} from './financial-pivot-header/financial-pivot-header';

defineFinancialPivotHeaderElement();

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'financial-pivot-header': DetailedHTMLProps<
        HTMLAttributes<FinancialPivotHeaderElement>,
        FinancialPivotHeaderElement
      >;
    }
  }
}

interface PivotProps {
  rows?: DataType[];
  initialPreset?: FinancialPresetId;
  chartTitle?: string;
}

function PivotShowcase({ rows, initialPreset = 'sales', chartTitle }: PivotProps) {
  const [isDark, setIsDark] = useState(() => currentTheme().isDark());
  const initialData = useMemo(() => resolveFinancialRows(rows), [rows]);
  const [data, setData] = useState<DataType[]>(initialData);
  const [loading, setLoading] = useState(!rows);
  const [loadError, setLoadError] = useState('');
  const [activePreset, setActivePreset] = useState<FinancialPresetId>(initialPreset);
  const [pivotConfig, setPivotConfig] = useState<PivotConfig>(() => restorePivotConfig(activePreset));
  const [configuratorVisible, setConfiguratorVisible] = useState(false);
  const [configuratorMount, setConfiguratorMount] = useState<HTMLElement>();
  const fieldsDrawer = useRef<FinancialFieldsDrawer>();
  const [expanded, setExpanded] = useState(false);
  const [chartVisible, setChartVisible] = useState(false);
  const [savedAvailable, setSavedAvailable] = useState(hasSavedPivotReport);
  const [reportsRevision, setReportsRevision] = useState(0);
  const [actionError, setActionError] = useState('');
  const [columnSize, setColumnSize] = useState(128);
  const [hostWidth, setHostWidth] = useState(0);
  const chartContainer = useRef<HTMLDivElement>(null);
  const chartRef = useRef<PivotChartRef>();

  const pivot = useMemo(
    () => applyFinancialPivotOptions(pivotConfig, data, configuratorVisible, configuratorMount, hostWidth),
    [pivotConfig, data, configuratorVisible, configuratorMount, hostWidth],
  );
  const plugins = useMemo(() => FINANCIAL_SHOWCASE_PLUGINS, []);
  const columns = useMemo(() => FINANCIAL_COLUMNS, []);
  const columnTypes = useMemo(() => FINANCIAL_COLUMN_TYPES, []);
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const gridHostRef = useRef<HTMLDivElement>(null);
  const headerRef = useRef<FinancialPivotHeaderElement>(null);
  const summaryMountRef = useRef<HTMLDivElement>(null);

  useEffect(() => observeCurrentTheme(setIsDark), []);
  useEffect(() => {
    const host = gridHostRef.current;
    if (!host) return;
    const fit = () => {
      setHostWidth(host.clientWidth);
      setColumnSize(financialPivotColumnSize(host, pivotConfig));
    };
    const observer = new ResizeObserver(fit);
    observer.observe(host);
    fit();
    return () => observer.disconnect();
  }, [loading, pivotConfig]);
  useEffect(() => {
    const drawer = createFinancialFieldsDrawer(() => setConfiguratorVisible(false));
    fieldsDrawer.current = drawer;
    setConfiguratorMount(drawer.mountTo);
    return () => { drawer.destroy(); fieldsDrawer.current = undefined; };
  }, []);

  useEffect(() => {
    if (rows) { setData(initialData); setLoading(false); return; }
    let cancelled = false;
    setLoading(true);
    loadContosoData().then((dataset) => {
      if (cancelled) return;
      setData(dataset.rows);
      setLoading(false);
    }).catch((error) => {
      if (cancelled) return;
      setLoadError(`Could not load the Contoso dataset: ${error instanceof Error ? error.message : String(error)}`);
      setLoading(false);
    });
    return () => { cancelled = true; };
  }, [initialData, rows]);

  useEffect(() => () => chartRef.current?.destroy(), []);

  useEffect(() => {
    if (!chartVisible || chartRef.current || !chartContainer.current || !gridRef.current) return;
    let cancelled = false;
    mountFinancialChart(gridRef.current, chartContainer.current, chartTitle).then((chart) => {
      if (cancelled) chart.destroy();
      else chartRef.current = chart;
    }).catch((error) => {
      if (cancelled) return;
      setChartVisible(false);
      setActionError(financialActionError(error));
    });
    return () => { cancelled = true; };
  }, [chartVisible, chartTitle]);

  useEffect(() => {
    const grid = gridRef.current;
    if (!grid) return;
    const handler = (event: Event) => {
      const nextConfig = (event as CustomEvent<PivotConfig>).detail || createFinancialPreset();
      setPivotConfig(nextConfig);
    };
    grid.addEventListener('pivot-config-update', handler);
    return () => grid.removeEventListener('pivot-config-update', handler);
  }, []);

  const selectPreset = useCallback((id: FinancialPresetId) => {
    if (gridRef.current) gridRef.current.pivot = undefined;
    window.setTimeout(() => {
      setPivotConfig(createFinancialPreset(id));
      setActivePreset(id);
    });
  }, []);

  const headerState = useMemo<FinancialPivotHeaderState>(() => ({
    activePreset,
    configuratorVisible,
    expanded,
    chartVisible,
    savedAvailable,
    savedReports: listSavedPivotReports(),
  }), [activePreset, configuratorVisible, expanded, chartVisible, savedAvailable, pivotConfig, reportsRevision]);

  useEffect(() => {
    if (headerRef.current) headerRef.current.state = headerState;
  }, [headerState]);

  useEffect(() => {
    if (gridRef.current && summaryMountRef.current) {
      gridRef.current.pivotSummaryPanel = { mountTo: summaryMountRef.current };
    }
  }, [loading, loadError]);

  useEffect(() => {
    const header = headerRef.current;
    if (!header) return;
    const onPreset = (event: Event) => {
      selectPreset((event as CustomEvent<FinancialPresetId>).detail);
    };
    const onConfigurator = () => {
      if (configuratorVisible) fieldsDrawer.current?.hide();
      else {
        fieldsDrawer.current?.show(document.activeElement as HTMLElement);
        setConfiguratorVisible(true);
      }
    };
    const onExpanded = () => setExpanded((value) => !value);
    const onAction = async (event: Event) => {
      const action = (event as CustomEvent<FinancialPivotAction>).detail;
      setActionError('');
      try {
        if (action === 'export-json' || typeof action === 'object') {
          const result = handleReportAction(action, pivotConfig, activePreset);
          if (result.restored) {
            if (gridRef.current) gridRef.current.pivot = undefined;
            window.setTimeout(() => { setPivotConfig(result.restored!.config); setActivePreset(result.restored!.preset); });
          }
          setSavedAvailable(hasSavedPivotReport());
          setReportsRevision((value) => value + 1);
          setActionError(result.error ?? '');
          return;
        }
        if (action === 'reset') selectPreset(initialPreset);
        if (action === 'save') {
          const saved = persistPivotState(pivotConfig, activePreset);
          setSavedAvailable(saved);
          if (!saved) setActionError('This browser could not save the report.');
        }
        if (action === 'restore') {
          const saved = loadSavedPivotReport();
          if (saved) {
            if (gridRef.current) gridRef.current.pivot = undefined;
            window.setTimeout(() => { setPivotConfig(saved.config); setActivePreset(saved.preset); });
          } else setActionError('The saved report could not be restored.');
        }
        if (action === 'export' && gridRef.current) await exportFinancialCsv(gridRef.current);
        if (action === 'export-xlsx' && gridRef.current) await exportFinancialXlsx(gridRef.current);
        if (action === 'print' && gridRef.current) await printFinancialPivot(gridRef.current);
        if (action === 'chart') setChartVisible((value) => !value);
      } catch {
        setActionError('Could not complete this report action. Please try again.');
      }
    };
    header.addEventListener(FINANCIAL_PIVOT_PRESET_EVENT, onPreset);
    header.addEventListener(FINANCIAL_PIVOT_CONFIGURATOR_EVENT, onConfigurator);
    header.addEventListener(FINANCIAL_PIVOT_EXPANDED_EVENT, onExpanded);
    header.addEventListener(FINANCIAL_PIVOT_ACTION_EVENT, onAction);
    return () => {
      header.removeEventListener(FINANCIAL_PIVOT_PRESET_EVENT, onPreset);
      header.removeEventListener(FINANCIAL_PIVOT_CONFIGURATOR_EVENT, onConfigurator);
      header.removeEventListener(FINANCIAL_PIVOT_EXPANDED_EVENT, onExpanded);
      header.removeEventListener(FINANCIAL_PIVOT_ACTION_EVENT, onAction);
    };
  }, [selectPreset, pivotConfig, activePreset, configuratorVisible]);

  return (
    <div
      className={`financial-pivot-showcase grow flex flex-col gap-2 h-full p-2 box-border${chartVisible ? ' financial-pivot-showcase--chart' : ''}`}
      data-expanded={expanded}
    >
      <financial-pivot-header ref={headerRef} />
      <div ref={summaryMountRef} />
      {actionError && <div role="alert" className="financial-pivot-header__question">{actionError}</div>}
      {loading && <div role="status" className="financial-pivot-header__question">Loading Contoso retail data…</div>}
      {loadError && <div role="alert" className="financial-pivot-header__question">{loadError}</div>}
      <div ref={chartContainer} className="financial-pivot-chart" aria-label="Pivot chart" hidden={!chartVisible} />

      <div className="financial-pivot-grid-view grow min-h-0 overflow-auto" aria-hidden={chartVisible}>
        <div ref={gridHostRef} className="pivot-grid-container h-full overflow-hidden">
          <RevoGrid
            ref={gridRef}
            className="overflow-hidden skip-style h-full min-h-0 cell-border"
            hideAttribution
            range
            resize
            filter
            multiRowHeader={FINANCIAL_MULTI_ROW_HEADER}
            colSize={columnSize}
            source={data}
            columns={columns}
            pivot={pivot}
            pivotCharts={FINANCIAL_PIVOT_CHARTS}
            pivotChartsUi={FINANCIAL_PIVOT_CHARTS_UI}
            theme={isDark ? 'darkCompact' : 'compact'}
            plugins={plugins}
            columnTypes={columnTypes}
            readonly
          />
        </div>
      </div>
    </div>
  );
}

export default PivotShowcase;
Vuevue
<template>
  <div
    class="financial-pivot-showcase grow flex flex-col gap-2 h-full box-border"
    :class="{ 'financial-pivot-showcase--chart': chartVisible }"
    :data-expanded="expanded"
  >
    <financial-pivot-header
      :state.prop="headerState"
      @financial-pivot-preset-select="onPresetSelect"
      @financial-pivot-configurator-toggle="toggleFields"
      @financial-pivot-expanded-toggle="expanded = !expanded"
      @financial-pivot-action="onAction"
    />
    <div ref="summaryMount" />
    <div v-if="loading" role="status" class="financial-pivot-header__question">Loading Contoso retail data…</div>
    <div v-if="loadError" role="alert" class="financial-pivot-header__question">{{ loadError }}</div>
    <div v-if="actionError" role="alert" class="financial-pivot-header__question">{{ actionError }}</div>
    <div v-show="chartVisible" ref="chartContainer" class="financial-pivot-chart" aria-label="Pivot chart" />

    <div v-if="!loading && !loadError" class="financial-pivot-grid-view grow min-h-0 overflow-auto" :aria-hidden="chartVisible">
      <div
        ref="gridHost"
        class="pivot-grid-container h-full overflow-hidden gap-1"
      >
        <RevoGrid
          class="overflow-hidden skip-style h-full min-h-0 cell-border"
          hide-attribution
          range
          resize
          filter
          :multi-row-header.prop="FINANCIAL_MULTI_ROW_HEADER"
          :colSize="columnSize"
          :source="rows"
          :columns="FINANCIAL_COLUMNS"
          :pivot.prop="pivot"
          :pivot-summary-panel.prop="summaryOptions"
          :pivot-charts.prop="FINANCIAL_PIVOT_CHARTS"
          :pivot-charts-ui.prop="FINANCIAL_PIVOT_CHARTS_UI"
          :theme="isDark ? 'darkCompact' : 'compact'"
          :plugins="plugins"
          :column-types="columnTypes"
          readonly
          @aftergridinit="onGridReady"
          @pivot-config-update="configUpdate"
        />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import type { PivotConfig, PivotChartRef } from '@revolist/pivot';
import { currentTheme, observeCurrentTheme } from './shared/theme';
import { loadContosoData } from './contoso-data';
import {
  hasSavedPivotReport,
  handleReportAction,
  listSavedPivotReports,
  loadSavedPivotReport,
  persistPivotState,
  restoreActivePreset,
  restorePivotConfig,
} from './financial.analytics';
import { exportFinancialCsv, exportFinancialXlsx, financialActionError, mountFinancialChart, printFinancialPivot } from './financial.actions';
import { createFinancialFieldsDrawer, type FinancialFieldsDrawer } from './financial-fields-drawer';
import {
  FINANCIAL_COLUMNS,
  FINANCIAL_COLUMN_TYPES,
  FINANCIAL_MULTI_ROW_HEADER,
  FINANCIAL_PIVOT_CHARTS,
  FINANCIAL_PIVOT_CHARTS_UI,
  FINANCIAL_SHOWCASE_PLUGINS,
  applyFinancialPivotOptions,
  createFinancialPreset,
  financialPivotColumnSize,
  resolveFinancialRows,
  type FinancialPresetId,
} from './financial.pivot';
import {
  defineFinancialPivotHeaderElement,
  type FinancialPivotAction,
  type FinancialPivotHeaderState,
} from './financial-pivot-header/financial-pivot-header';

defineFinancialPivotHeaderElement();

const isDark = ref(currentTheme().isDark());
let disconnectTheme: (() => void) | undefined;
const props = defineProps<{ rows?: any[]; initialPreset?: FinancialPresetId; chartTitle?: string }>();
const initialRows = resolveFinancialRows(props.rows);
const rows = shallowRef(initialRows);
const loading = ref(!props.rows);
const loadError = ref('');
const actionError = ref('');
let fieldsDrawer: FinancialFieldsDrawer | undefined;
const configuratorMount = shallowRef<HTMLElement>();

onMounted(() => {
  fieldsDrawer = createFinancialFieldsDrawer(() => { configuratorVisible.value = false; });
  configuratorMount.value = fieldsDrawer.mountTo;
  disconnectTheme = observeCurrentTheme((value) => {
    isDark.value = value;
  });
  if (!props.rows) {
    void loadContosoData().then((data) => {
      rows.value = data.rows;
    }).catch((error) => {
      loadError.value = `Could not load the Contoso dataset: ${error instanceof Error ? error.message : String(error)}`;
    }).finally(() => { loading.value = false; });
  }
});
onBeforeUnmount(() => { disconnectTheme?.(); fieldsDrawer?.destroy(); gridResizeObserver?.disconnect(); });

const activePreset = ref<FinancialPresetId>(props.initialPreset ?? restoreActivePreset());
const pivotConfig = shallowRef<PivotConfig>(restorePivotConfig(activePreset.value));
const configuratorVisible = ref(false);
const expanded = ref(false);
const chartVisible = ref(false);
const chartContainer = ref<HTMLElement>();
let chart: PivotChartRef | undefined;
const savedAvailable = ref(hasSavedPivotReport());
const reportsRevision = ref(0);
const gridHost = ref<HTMLElement>();
const columnSize = ref(128);
const hostWidth = ref(0);
// Resolve the public element when the wrapper has finished initializing the grid.
const gridElement = shallowRef<HTMLRevoGridElement>();
const onGridReady = (event: Event) => {
  gridElement.value = event.target as HTMLRevoGridElement;
};
watch([gridElement, chartVisible], async ([grid, visible], _previous, onCleanup) => {
  if (!grid || !visible || chart || !chartContainer.value) return;
  let active = true;
  onCleanup(() => { active = false; });
  try {
    await grid.componentOnReady();
    if (!active) return;
    const mounted = await mountFinancialChart(grid, chartContainer.value, props.chartTitle);
    if (active) chart = mounted;
    else mounted.destroy();
  } catch (error) {
    if (active) {
      chartVisible.value = false;
      actionError.value = financialActionError(error);
    }
  }
}, { flush: 'post' });
let gridResizeObserver: ResizeObserver | undefined;
const fitColumns = () => {
  if (gridHost.value) {
    hostWidth.value = gridHost.value.clientWidth;
    columnSize.value = financialPivotColumnSize(gridHost.value, pivotConfig.value);
  }
};
watch(gridHost, (host) => {
  gridResizeObserver?.disconnect();
  if (!host) return;
  gridResizeObserver = new ResizeObserver(fitColumns);
  gridResizeObserver.observe(host);
  void nextTick(fitColumns);
});
watch(pivotConfig, () => { void nextTick(fitColumns); });
const columnTypes = ref(FINANCIAL_COLUMN_TYPES);
const plugins = FINANCIAL_SHOWCASE_PLUGINS;
const summaryMount = shallowRef<HTMLElement>();
const summaryOptions = computed(() => ({ mountTo: summaryMount.value }));
const headerState = computed<FinancialPivotHeaderState>(() => ({
  activePreset: activePreset.value,
  configuratorVisible: configuratorVisible.value,
  expanded: expanded.value,
  chartVisible: chartVisible.value,
  savedAvailable: savedAvailable.value,
  savedReports: (reportsRevision.value, listSavedPivotReports()),
}));

const pivot = computed(() =>
  applyFinancialPivotOptions(
    pivotConfig.value,
    rows.value,
    configuratorVisible.value,
    configuratorMount.value,
    hostWidth.value,
  ),
);

const toggleFields = () => {
  if (configuratorVisible.value) fieldsDrawer?.hide();
  else {
    fieldsDrawer?.show(document.activeElement as HTMLElement);
    configuratorVisible.value = true;
  }
};

const configUpdate = (event: CustomEvent<PivotConfig>) => {
  pivotConfig.value = event.detail || createFinancialPreset();
};

const selectPreset = (id: FinancialPresetId) => {
  if (gridElement.value) gridElement.value.pivot = undefined;
  window.setTimeout(() => {
    pivotConfig.value = createFinancialPreset(id);
    activePreset.value = id;
  });
};

const onPresetSelect = (event: Event) => {
  selectPreset((event as CustomEvent<FinancialPresetId>).detail);
};

const onAction = async (event: Event) => {
  const action = (event as CustomEvent<FinancialPivotAction>).detail;
  actionError.value = '';
  try {
    if (action === 'export-json' || typeof action === 'object') {
      const result = handleReportAction(action, pivotConfig.value, activePreset.value);
      if (result.restored) {
        if (gridElement.value) gridElement.value.pivot = undefined;
        window.setTimeout(() => {
          pivotConfig.value = result.restored!.config;
          activePreset.value = result.restored!.preset;
        });
      }
      savedAvailable.value = hasSavedPivotReport();
      reportsRevision.value++;
      actionError.value = result.error ?? '';
      return;
    }
    if (action === 'reset') selectPreset(props.initialPreset ?? 'sales');
    if (action === 'save') {
      savedAvailable.value = persistPivotState(pivotConfig.value, activePreset.value);
      if (!savedAvailable.value) actionError.value = 'This browser could not save the report.';
    }
    if (action === 'restore') {
      const saved = loadSavedPivotReport();
      if (saved) {
        if (gridElement.value) gridElement.value.pivot = undefined;
        window.setTimeout(() => {
          pivotConfig.value = saved.config;
          activePreset.value = saved.preset;
        });
      } else actionError.value = 'The saved report could not be restored.';
    }
    if (action === 'export' && gridElement.value) await exportFinancialCsv(gridElement.value);
    if (action === 'export-xlsx' && gridElement.value) await exportFinancialXlsx(gridElement.value);
    if (action === 'print' && gridElement.value) await printFinancialPivot(gridElement.value);
    if (action === 'chart') {
      chartVisible.value = !chartVisible.value;
    }
  } catch {
    actionError.value = 'Could not complete this report action. Please try again.';
  }
};
onBeforeUnmount(() => chart?.destroy());
</script>

<style lang="scss">
@use './financial-pivot-header/financial-pivot-header.scss';

revo-grid.cell-border .rgHeaderCell[highlight] {
  box-shadow: 0 -3px 0 0 #00b997 inset, -1px 0 0 0 var(--revo-grid-cell-border) inset;
}
</style>
Angularts
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  AfterViewInit,
  ElementRef,
  Input,
  NO_ERRORS_SCHEMA,
  OnDestroy,
  OnInit,
  signal,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import { NgIf } from '@angular/common';
import { RevoGrid, type DataType } from '@revolist/angular-datagrid';
import type { PivotConfig, PivotChartRef } from '@revolist/pivot';
import { currentTheme, observeCurrentTheme } from './shared/theme';
import { loadContosoData } from './contoso-data';
import {
  hasSavedPivotReport,
  handleReportAction,
  listSavedPivotReports,
  loadSavedPivotReport,
  persistPivotState,
  restoreActivePreset,
  restorePivotConfig,
} from './financial.analytics';
import { exportFinancialCsv, exportFinancialXlsx, financialActionError, mountFinancialChart, printFinancialPivot } from './financial.actions';
import { createFinancialFieldsDrawer, type FinancialFieldsDrawer } from './financial-fields-drawer';
import {
  FINANCIAL_COLUMNS,
  FINANCIAL_COLUMN_TYPES,
  FINANCIAL_MULTI_ROW_HEADER,
  FINANCIAL_PIVOT_CHARTS,
  FINANCIAL_PIVOT_CHARTS_UI,
  FINANCIAL_SHOWCASE_PLUGINS,
  applyFinancialPivotOptions,
  createFinancialPreset,
  financialPivotColumnSize,
  resolveFinancialRows,
  type FinancialPresetId,
} from './financial.pivot';
import {
  defineFinancialPivotHeaderElement,
  type FinancialPivotAction,
  type FinancialPivotHeaderState,
} from './financial-pivot-header/financial-pivot-header';

defineFinancialPivotHeaderElement();

@Component({
  selector: 'pivot-showcase-grid',
  standalone: true,
  imports: [RevoGrid, NgIf],
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  styleUrls: ['./financial-pivot-header/financial-pivot-header.scss'],
  schemas: [NO_ERRORS_SCHEMA],
  template: `
    <div
      class="financial-pivot-showcase grow flex flex-col gap-2 h-full p-2 box-border"
      [class.financial-pivot-showcase--chart]="chartVisible()"
      [attr.data-expanded]="expanded()"
    >
      <financial-pivot-header
        [state]="headerState"
        (financial-pivot-preset-select)="selectPreset($any($event).detail)"
        (financial-pivot-configurator-toggle)="toggleFields()"
        (financial-pivot-expanded-toggle)="expanded.set(!expanded())"
        (financial-pivot-action)="onAction($any($event).detail)"
      ></financial-pivot-header>
      <div #summaryMount></div>
      <div *ngIf="loading()" role="status" class="financial-pivot-header__question">Loading Contoso retail data…</div>
      <div *ngIf="loadError()" role="alert" class="financial-pivot-header__question">{{ loadError() }}</div>
      <div *ngIf="actionError()" role="alert" class="financial-pivot-header__question">{{ actionError() }}</div>
      <div #chartContainer class="financial-pivot-chart" aria-label="Pivot chart" [hidden]="!chartVisible()"></div>

      <div class="financial-pivot-grid-view grow min-h-0 overflow-auto" [attr.aria-hidden]="chartVisible() ? 'true' : null">
        <div
          #gridHost class="pivot-grid-container h-full overflow-hidden"
        >
          <revo-grid
            #gridElement
            class="overflow-hidden skip-style h-full min-h-0 cell-border"
            [hideAttribution]="true"
            [range]="true"
            [resize]="true"
            [filter]="true"
            [multiRowHeader]="multiRowHeader"
            [colSize]="columnSize()"
            [source]="sourceRows()"
            [columns]="FINANCIAL_COLUMNS"
            [pivot]="pivot()"
            [pivotSummaryPanel]="{ mountTo: summaryMount }"
            [pivotCharts]="pivotCharts"
            [pivotChartsUi]="pivotChartsUi"
            [theme]="theme()"
            [plugins]="plugins"
            [columnTypes]="columnTypes"
            [readonly]="true"
            (pivot-config-update)="configUpdate($event)"
          ></revo-grid>
        </div>
      </div>
    </div>
  `,
})
export class PivotShowcaseGridComponent implements OnInit, AfterViewInit, OnDestroy {
  @Input() chartTitle?: string;
  private initialPresetValue: FinancialPresetId = 'sales';
  @Input() set initialPreset(value: FinancialPresetId) {
    if (value && value !== this.initialPresetValue) {
      this.initialPresetValue = value;
      this.selectPreset(value);
    }
  }
  readonly sourceRows = signal<DataType[]>(resolveFinancialRows());
  private suppliedRows = false;
  private disposed = false;

  @Input() set rows(value: DataType[]) {
    this.suppliedRows = true;
    this.sourceRows.set(resolveFinancialRows(value));
  }

  readonly loading = signal(true);
  readonly loadError = signal('');
  readonly actionError = signal('');

  ngOnInit(): void {
    if (this.suppliedRows) { this.loading.set(false); return; }
    void loadContosoData().then((dataset) => {
      if (this.disposed) return;
      this.sourceRows.set(dataset.rows);
      this.loading.set(false);
    }).catch((error) => {
      if (this.disposed) return;
      this.loadError.set(`Could not load the Contoso dataset: ${error instanceof Error ? error.message : String(error)}`);
      this.loading.set(false);
    });
  }

  @ViewChild('gridElement', { read: ElementRef })
  gridElement?: ElementRef<HTMLRevoGridElement & { pivot?: PivotConfig }>;
  @ViewChild('gridHost', { read: ElementRef }) gridHost?: ElementRef<HTMLElement>;
  private gridResizeObserver?: ResizeObserver;
  private fitColumns() {
    if (this.gridHost) {
      this.hostWidth.set(this.gridHost.nativeElement.clientWidth);
      this.columnSize.set(financialPivotColumnSize(this.gridHost.nativeElement, this.pivotSignal()));
    }
  }

  readonly FINANCIAL_COLUMNS = FINANCIAL_COLUMNS;
  readonly columnSize = signal(128);
  readonly hostWidth = signal(0);
  readonly columnTypes = FINANCIAL_COLUMN_TYPES;
  readonly multiRowHeader = FINANCIAL_MULTI_ROW_HEADER;
  readonly pivotCharts = FINANCIAL_PIVOT_CHARTS;
  readonly pivotChartsUi = FINANCIAL_PIVOT_CHARTS_UI;
  readonly plugins = FINANCIAL_SHOWCASE_PLUGINS;
  readonly isDark = signal(currentTheme().isDark());
  readonly theme = computed(() => this.isDark() ? 'darkCompact' : 'compact');
  private readonly disconnectTheme = observeCurrentTheme((value) => this.isDark.set(value));

  readonly activePreset = signal<FinancialPresetId>(restoreActivePreset());
  readonly pivotSignal = signal<PivotConfig>(restorePivotConfig(this.activePreset()));
  readonly configuratorVisible = signal(false);
  readonly configuratorMount = signal<HTMLElement | undefined>(undefined);
  private fieldsDrawer?: FinancialFieldsDrawer;
  readonly expanded = signal(false);

  readonly chartVisible = signal(false);
  readonly savedAvailable = signal(hasSavedPivotReport());
  readonly reportsRevision = signal(0);
  private chart?: PivotChartRef;

  @ViewChild('chartContainer', { read: ElementRef }) chartContainer?: ElementRef<HTMLElement>;

  get headerState(): FinancialPivotHeaderState {
    this.reportsRevision();
    return {
      activePreset: this.activePreset(),
      configuratorVisible: this.configuratorVisible(),
      expanded: this.expanded(),
      chartVisible: this.chartVisible(),
      savedAvailable: this.savedAvailable(),
      savedReports: listSavedPivotReports(),
    };
  }

  readonly pivot = computed(() => applyFinancialPivotOptions(
    this.pivotSignal(),
    this.sourceRows(),
    this.configuratorVisible(),
    this.configuratorMount(),
    this.hostWidth(),
  ));

  ngAfterViewInit(): void {
    this.fieldsDrawer = createFinancialFieldsDrawer(() => this.configuratorVisible.set(false));
    this.configuratorMount.set(this.fieldsDrawer.mountTo);
    if (this.gridHost) {
      this.gridResizeObserver = new ResizeObserver(() => this.fitColumns());
      this.gridResizeObserver.observe(this.gridHost.nativeElement);
      this.fitColumns();
    }
  }

  toggleFields() {
    if (this.configuratorVisible()) this.fieldsDrawer?.hide();
    else {
      this.fieldsDrawer?.show(document.activeElement as HTMLElement);
      this.configuratorVisible.set(true);
    }
  }

  configUpdate(event: CustomEvent<PivotConfig>) {
    this.pivotSignal.set(event.detail || createFinancialPreset());
    this.fitColumns();
  }

  selectPreset(id: FinancialPresetId) {
    this.replacePivot(createFinancialPreset(id), id);
  }

  async onAction(action: FinancialPivotAction) {
    this.actionError.set('');
    try {
      if (action === 'export-json' || typeof action === 'object') {
        const result = handleReportAction(action, this.pivotSignal(), this.activePreset());
        if (result.restored) this.replacePivot(result.restored.config, result.restored.preset);
        this.reportsRevision.update((value) => value + 1);
        this.actionError.set(result.error ?? '');
        return;
      }
      if (action === 'reset') this.selectPreset(this.initialPresetValue);
      if (action === 'save') {
        const saved = persistPivotState(this.pivotSignal(), this.activePreset());
        this.savedAvailable.set(saved);
        if (!saved) this.actionError.set('This browser could not save the report.');
      }
      if (action === 'restore') {
        const saved = loadSavedPivotReport();
        if (saved) this.replacePivot(saved.config, saved.preset);
        else this.actionError.set('The saved report could not be restored.');
      }
      if (action === 'export' && this.gridElement) await exportFinancialCsv(this.gridElement.nativeElement);
      if (action === 'export-xlsx' && this.gridElement) await exportFinancialXlsx(this.gridElement.nativeElement);
      if (action === 'print' && this.gridElement) await printFinancialPivot(this.gridElement.nativeElement);
      if (action === 'chart') {
        this.chartVisible.set(!this.chartVisible());
        if (this.chartVisible() && !this.chart && this.gridElement && this.chartContainer) {
          this.chart = await mountFinancialChart(this.gridElement.nativeElement, this.chartContainer.nativeElement, this.chartTitle);
        }
      }
    } catch (error) {
      if (action === 'chart') this.chartVisible.set(false);
      this.actionError.set(financialActionError(error));
    }
  }

  private replacePivot(config: PivotConfig, preset: FinancialPresetId) {
    if (this.gridElement) this.gridElement.nativeElement.pivot = undefined;
    window.setTimeout(() => {
      this.pivotSignal.set(config);
      this.activePreset.set(preset);
      this.fitColumns();
    });
  }

  ngOnDestroy(): void {
    this.fieldsDrawer?.destroy();
    this.gridResizeObserver?.disconnect();
    this.disposed = true;
    this.chart?.destroy();
    this.disconnectTheme();
  }
}
Pivot Configts
/** Shared report model for Main and Financial: dimensions, presets, plugin options and column sizing. Framework files only wire UI state and lifecycle. */
import type { ColumnRegular, DataType, GridPlugin } from '@revolist/revogrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import * as ProPlugins from '@revolist/revogrid-pro';
import {
  PivotChartsPlugin,
  PivotChartsUiPlugin,
  type PivotConfig,
  type PivotConfigDimension,
  clonePivotFilterSelectionMap,
  createPivotChartsRenderer,
  filterPivotSource,
  PivotPlugin,
  type PivotChartsConfig,
  type PivotChartsUiConfig,
} from '@revolist/pivot';
import {
  AdvanceFilterPlugin,
  FilterHeaderPlugin,
  ContextMenuPlugin,
  RowOddPlugin,
  RowSelectPlugin,
  SameValueMergePlugin,
  commonAggregators,
  columnTypeRenderer,
} from '@revolist/revogrid-pro';
import { createFinancialHeatmapColumnType } from './financial.heatmap';

export const FINANCIAL_COLUMN_TYPES = {
  currency: new NumberColumnType('$0,0.00'),
  number: new NumberColumnType('0,0'),
  integer: new NumberColumnType('0,0'),
  salesHeatmap: createFinancialHeatmapColumnType('Revenue', 'currency'),
  profitHeatmap: createFinancialHeatmapColumnType('Gross Profit', 'currency'),
  unitsHeatmap: createFinancialHeatmapColumnType('Quantity', 'number'),
  costHeatmap: createFinancialHeatmapColumnType('Cost', 'currency'),
};

const percentile90 = (values: any[]) => {
  if (!values.length) return 0;
  const sorted = values.map(Number).sort((left, right) => left - right);
  return sorted[Math.ceil(sorted.length * 0.9) - 1] ?? 0;
};

const currencyAggregators = {
  sum: commonAggregators.sum,
  avg: commonAggregators.avg,
  min: commonAggregators.min,
  max: commonAggregators.max,
  p90: percentile90,
};

const numberAggregators = {
  sum: commonAggregators.sum,
  avg: commonAggregators.avg,
};

const MONTH_ORDER = [
  'January', 'February', 'March', 'April', 'May', 'June',
  'July', 'August', 'September', 'October', 'November', 'December',
];

const monthCompare: NonNullable<PivotConfigDimension['cellCompare']> = (_prop, a, b) =>
  MONTH_ORDER.indexOf(String(a.Month)) - MONTH_ORDER.indexOf(String(b.Month));

const FINANCIAL_DIMENSION_DEFINITIONS: PivotConfigDimension[] = [
  { prop: 'Country', description: 'Customer country', fieldGroup: 'Dimensions', sortable: true, order: 'asc', merge: true, filter: ['string', 'selection'] },
  { prop: 'Store Country', description: 'Store country, separate from customer country', fieldGroup: 'Dimensions', sortable: true, order: 'asc', merge: true, filter: ['string', 'selection'] },
  { prop: 'Store', fieldGroup: 'Dimensions', sortable: true, filter: ['string', 'selection'] },
  { prop: 'Category', fieldGroup: 'Dimensions', sortable: true, order: 'asc', filter: ['string', 'selection'], size: 275 },
  { prop: 'Subcategory', fieldGroup: 'Dimensions', sortable: true, order: 'asc', filter: ['string', 'selection'] },
  { prop: 'Brand', fieldGroup: 'Dimensions', sortable: true, order: 'asc', filter: ['string', 'selection'] },
  { prop: 'Year', fieldGroup: ['Dimensions', 'Date'], sortable: true, order: 'asc', columnType: 'integer', filter: ['number', 'selection'] },
  { prop: 'Quarter', fieldGroup: ['Dimensions', 'Date'], sortable: true, order: 'asc', filter: ['string', 'selection'], filterOptions: ['Q1', 'Q2', 'Q3', 'Q4'] },
  { prop: 'Month Number', fieldGroup: ['Dimensions', 'Date'], sortable: true, order: 'asc', columnType: 'integer' },
  {
    prop: 'Month',
    fieldGroup: ['Dimensions', 'Date'],
    sortable: true,
    order: 'asc',
    cellCompare: monthCompare,
    filter: ['string', 'selection'],
    filterOptions: [...MONTH_ORDER],
  },
  {
    prop: 'Product',
    // Keep product names recognizable while leaving room for a value on mobile.
    size: 275,
    fieldGroup: 'Dimensions',
    sortable: true,
    filter: ['string', 'selection'],
  },
  {
    prop: 'Quantity',
    fieldGroup: 'Data',
    sortable: true,
    columnType: 'unitsHeatmap',
    filter: ['number'],
    aggregators: numberAggregators,
  },
  {
    prop: 'Revenue',
    fieldGroup: 'Data',
    sortable: true,
    columnType: 'currency',
    filter: ['number'],
    aggregators: currencyAggregators,
  },
  {
    prop: 'Gross Profit',
    fieldGroup: 'Data',
    sortable: true,
    columnType: 'profitHeatmap',
    filter: ['number'],
    aggregators: currencyAggregators,
  },
  {
    prop: 'Cost',
    fieldGroup: 'Data',
    sortable: true,
    columnType: 'costHeatmap',
    filter: ['number'],
    aggregators: currencyAggregators,
  },
  { prop: 'Date', fieldGroup: 'Dimensions', sortable: true, filter: ['string'] },
  { prop: 'Order ID', fieldGroup: 'Dimensions', sortable: true, filter: ['string'] },
  { prop: 'Line ID', fieldGroup: 'Dimensions', sortable: true, filter: ['string'] },
  { prop: 'Order Currency', fieldGroup: 'Dimensions', sortable: true, filter: ['string', 'selection'] },
];

export const FINANCIAL_DIMENSIONS: PivotConfigDimension[] = FINANCIAL_DIMENSION_DEFINITIONS
  .map((dimension) => ({
    columnTemplate: columnTypeRenderer,
    ...dimension,
  }));

export const FINANCIAL_COLUMNS: ColumnRegular[] = FINANCIAL_DIMENSIONS.map((dimension) => ({
  ...dimension,
  size: dimension.size ?? 150,
}));

/** Developer opt-in for inline Pivot header inputs; report filters remain available when off. */
export const FINANCIAL_HEADER_FILTERS_ENABLED = false;

// The public trial 2.9.5 omits this optional enhancement. Namespace lookup keeps
// standalone builds compatible while enabling it when the installed API has it.
const summaryPanelPlugin = Reflect.get(ProPlugins, 'PivotSummaryPanelPlugin') as GridPlugin | undefined;

export function createFinancialShowcasePlugins(
  headerFiltersEnabled = FINANCIAL_HEADER_FILTERS_ENABLED,
  summaryPanelEnabled = true,
): GridPlugin[] {
  return [
    RowSelectPlugin,
    SameValueMergePlugin,
    ContextMenuPlugin,
    PivotPlugin,
    ...(summaryPanelEnabled && typeof summaryPanelPlugin === 'function' ? [summaryPanelPlugin] : []),
    PivotChartsPlugin,
    PivotChartsUiPlugin,
    AdvanceFilterPlugin,
    ...(headerFiltersEnabled ? [FilterHeaderPlugin] : []),
    RowOddPlugin,
  ] as GridPlugin[];
}

export const FINANCIAL_SHOWCASE_PLUGINS = createFinancialShowcasePlugins();

export const FINANCIAL_PIVOT_CHARTS: PivotChartsConfig = {
  renderer: createPivotChartsRenderer(),
  defaultChartType: 'groupedColumn',
  limits: {
    maxSeries: 120,
    maxDataPoints: 50_000,
  },
};

export const FINANCIAL_PIVOT_CHARTS_UI: PivotChartsUiConfig = {
  contextMenu: true,
};

export const FINANCIAL_MULTI_ROW_HEADER = {
  // Pivot row-axis headers contain sorting and filtering controls and should
  // retain their normal leaf-header geometry. Collapsed column groups opt in
  // to downward spanning independently through spanHeaderHeight.
  spanLeafHeaders: false,
} as const;

const SALES_OVERVIEW: PivotConfig = {
  dimensions: FINANCIAL_DIMENSIONS,
  rows: ['Country', 'Category'],
  columns: ['Quarter', 'Month'],
  values: [{ prop: 'Revenue', aggregator: 'sum', label: 'Revenue (USD)' }],
  filters: ['Year', 'Country', 'Category', 'Brand'],
  filterSelections: { Year: [2025] },
  hasConfigurator: true,
  contextMenu: { drillThrough: { columns: [
    'Order ID', 'Line ID', 'Date', 'Country', 'Store Country',
    'Category', 'Subcategory', 'Product', 'Brand', 'Quantity',
    'Revenue', 'Cost', 'Gross Profit',
  ] } },
  flatHeaders: false,
  collapsed: false,
  groupAggregations: true,
  columnCollapse: {
    enabled: true,
    // Reveal one hierarchy level at a time when fields are added or reordered.
    collapsed: true,
    placeholder: 'Quarter Total',
  },
  columnLevels: { 0: { collapsible: true, collapsed: true, filterable: false, sortable: false } },
  totals: {
    subtotals: false,
    grandTotal: true,
    grandTotalLabel: 'Grand Total',
  },
};

const PROFITABILITY: PivotConfig = {
  ...SALES_OVERVIEW,
  rows: ['Category', 'Subcategory'],
  columns: ['Year', 'Quarter'],
  values: [
    { prop: 'Revenue', aggregator: 'sum', label: 'Revenue (USD)' },
    { prop: 'Cost', aggregator: 'sum', label: 'Cost (USD)' },
    { prop: 'Gross Profit', aggregator: 'sum', label: 'Gross Profit (USD)' },
  ],
  valuesOnRows: true,
  filters: ['Year', 'Country', 'Category', 'Brand'],
  filterSelections: { Year: [2025] },
  columnCollapse: {
    enabled: true,
    collapsed: true,
    placeholder: 'Period Total',
  },
};

const PRODUCT_PERFORMANCE: PivotConfig = {
  ...SALES_OVERVIEW,
  rows: ['Category', 'Subcategory', 'Product'],
  columns: ['Year', 'Quarter'],
  values: [
    { prop: 'Revenue', aggregator: 'sum', label: 'Revenue (USD)' },
    { prop: 'Quantity', aggregator: 'sum', label: 'Quantity' },
  ],
  filters: ['Year', 'Country', 'Category', 'Brand'],
  filterSelections: { Year: [2025] },
  collapsed: true,
  expanded: {},
  columnCollapse: {
    enabled: true,
    collapsed: true,
    placeholder: 'Market Total',
  },
};

const ANALYSIS_BASE: PivotConfig = {
  ...SALES_OVERVIEW,
  rows: ['Category'],
  columns: ['Year'],
  filters: ['Year', 'Country', 'Category', 'Brand'],
  filterSelections: { Year: [2024, 2025] },
};

const PROFIT_ANALYSIS: PivotConfig = {
  ...ANALYSIS_BASE,
  values: [
    { id: 'profit', prop: 'Gross Profit', aggregator: 'sum', label: 'Gross Profit' },
    { id: 'sales', prop: 'Revenue', aggregator: 'sum', label: 'Revenue' },
  ],
};

const CONTRIBUTION_ANALYSIS: PivotConfig = {
  ...ANALYSIS_BASE,
  columns: [],
  values: [
    { id: 'profit-contribution', prop: 'Gross Profit', aggregator: 'sum', label: 'Profit share',
      calculation: { type: 'percentOfGrandTotal' }, format: { preset: 'percent' } },
    { id: 'profit', prop: 'Gross Profit', aggregator: 'sum', label: 'Gross Profit' },
  ],
};

const RUNNING_ANALYSIS: PivotConfig = {
  ...ANALYSIS_BASE,
  rows: ['Country'],
  columns: ['Month'],
  filters: ['Year', 'Product'],
  filterSelections: { Year: [2025] },
  values: [{ id: 'cumulative-sales', prop: 'Revenue', aggregator: 'sum', label: 'Revenue YTD',
    calculation: { type: 'runningTotal', axis: 'columns', field: 'Month' } }],
};

const YEAR_OVER_YEAR_ANALYSIS: PivotConfig = {
  ...ANALYSIS_BASE,
  rows: ['Product'],
  columns: ['Year'],
  // A collapsed year has two distinct measures but no meaningful single
  // carrier value. Keep both measures visible for the comparison report.
  columnLevels: { 0: { collapsible: false, collapsed: false, filterable: false, sortable: false } },
  values: [
    { id: 'sales', prop: 'Revenue', aggregator: 'sum', label: 'Revenue' },
    { id: 'sales-yoy', prop: 'Revenue', aggregator: 'sum', label: 'YoY change',
      calculation: { type: 'percentDifferenceFrom', axis: 'columns', field: 'Year', base: 'previous' },
      format: { preset: 'percent' } },
  ],
};

const RANKING_ANALYSIS: PivotConfig = {
  ...ANALYSIS_BASE,
  rows: ['Product'],
  columns: [],
  filters: ['Year', 'Country'],
  filterSelections: { Year: [2025] },
  values: [
    { id: 'profit', prop: 'Gross Profit', aggregator: 'sum', label: 'Gross Profit' },
    { id: 'profit-rank', prop: 'Gross Profit', aggregator: 'sum', label: 'Profit rank',
      calculation: { type: 'rank', axis: 'rows', field: 'Product', order: 'desc' },
      // Rank is a position, not the currency of its source profit measure.
      format: { preset: 'number', options: { maximumFractionDigits: 0 } } },
  ],
};

export type FinancialPresetId = 'sales' | 'profitability' | 'product'
  | 'profit' | 'contribution' | 'running' | 'yoy' | 'ranking';

export const FINANCIAL_ANALYSIS_PRESET_IDS: FinancialPresetId[] = [
  'profit', 'contribution', 'running', 'yoy', 'ranking',
];

export interface FinancialPreset {
  id: FinancialPresetId;
  label: string;
  description: string;
}

export const FINANCIAL_PRESETS: FinancialPreset[] = [
  {
    id: 'sales',
    label: 'Sales Overview',
    description: 'Revenue by customer country and category, across quarters · USD',
  },
  {
    id: 'profitability',
    label: 'Profitability',
    description: 'Compare revenue, cost and gross profit by product category and quarter.',
  },
  {
    id: 'product',
    label: 'Product Performance',
    description: 'Explore product revenue and quantity within categories and subcategories.',
  },
  { id: 'profit', label: 'Profit', description: 'How does gross profit vary across categories and years? Expand a year to compare its revenue.' },
  { id: 'contribution', label: 'Contribution', description: 'What share of total profit comes from each category?' },
  { id: 'running', label: 'Running total', description: 'How did 2025 revenue accumulate month by month in each country?' },
  { id: 'yoy', label: 'Year over year', description: 'Which products grew or declined between years? The first year has no prior comparison.' },
  { id: 'ranking', label: 'Ranking', description: 'Which products rank highest by 2025 profit?' },
];

const PRESET_CONFIGS: Record<FinancialPresetId, PivotConfig> = {
  sales: SALES_OVERVIEW,
  profitability: PROFITABILITY,
  product: PRODUCT_PERFORMANCE,
  profit: PROFIT_ANALYSIS,
  contribution: CONTRIBUTION_ANALYSIS,
  running: RUNNING_ANALYSIS,
  yoy: YEAR_OVER_YEAR_ANALYSIS,
  ranking: RANKING_ANALYSIS,
};

/** Semantic measure formats are shared by the grid and printable report. */
function financialMeasureFormat(value: PivotConfig['values'][number]): PivotConfig['values'][number]['format'] {
  if (value.format) return { ...value.format };
  if (['Revenue', 'Cost', 'Gross Profit'].includes(String(value.prop))) {
    return { preset: 'currency', currency: 'USD', locale: 'en-US' };
  }
  if (value.prop === 'Quantity') {
    return { preset: 'number', locale: 'en-US', options: { maximumFractionDigits: 0 } };
  }
  return undefined;
}

export function createFinancialPreset(id: FinancialPresetId = 'sales'): PivotConfig {
  const config = PRESET_CONFIGS[id];
  return {
    ...config,
    dimensions: config.dimensions?.map((dimension) => ({
      ...dimension,
      // The default quarter headers use the field panel for filtering. Avoid
      // an unusable filter input on the generated Grand Total column.
      ...(id === 'sales' && dimension.prop === 'Revenue' ? { filter: false } : {}),
    })),
    rows: [...config.rows],
    columns: [...(config.columns || [])],
    values: config.values.map((value) => ({
      ...value,
      calculation: value.calculation ? structuredClone(value.calculation) : undefined,
      // CSV/XLSX exports retain the original aggregate precision.
      format: financialMeasureFormat(value),
    })),
    filters: [...(config.filters || [])],
    filterSelections: clonePivotFilterSelectionMap(config.filterSelections || {}),
    columnLevels: config.columnLevels
      ? Object.fromEntries(
          Object.entries(config.columnLevels).map(([level, settings]) => [
            level,
            settings ? { ...settings } : settings,
          ]),
        )
      : undefined,
    totals: config.totals ? { ...config.totals } : undefined,
    columnCollapse: typeof config.columnCollapse === 'object'
      ? {
          ...config.columnCollapse,
          aggregator:
            typeof config.columnCollapse.aggregator === 'object'
              ? { ...config.columnCollapse.aggregator }
              : config.columnCollapse.aggregator,
        }
      : config.columnCollapse,
  };
}

export const FINANCIAL_SHOWCASE_PIVOT = createFinancialPreset();

/** Fit Sales Overview while reserving its row-label width in other compact reports. */
export function financialPivotColumnSize(host: HTMLElement, config: PivotConfig): number {
  const defaultSalesLayout = config.rows.join('|') === 'Country|Category'
    && config.columns?.join('|') === 'Quarter|Month'
    && config.values.length === 1
    && config.values[0]?.prop === 'Revenue';
  if (defaultSalesLayout) {
    return Math.max(130, Math.min(260, Math.floor((host.clientWidth - 275 - 2) / 5)));
  }
  return config.rows.length > 1 ? 275 : 128;
}

export function fitFinancialPivotColumns(grid: HTMLRevoGridElement, host: HTMLElement, config: PivotConfig): void {
  grid.colSize = financialPivotColumnSize(host, config);
}

export function resolveFinancialRows(rows?: DataType[]): DataType[] {
  return rows ?? [];
}

// Recover stale optional subtotal references in saved or already-mounted presets
// before applying UI changes such as field visibility. Keep other report choices.
export function reconcileFinancialSubtotalTargets(config: PivotConfig): PivotConfig {
  const next = { ...config };
  const disabled = config.totals?.disabledSubtotals;
  if (disabled) {
    next.totals = {
      ...config.totals,
      disabledSubtotals: Object.fromEntries(
        (['rows', 'columns'] as const).flatMap(axis => {
          const rule = disabled[axis];
          if (!rule) return [];
          const fields = config[axis] ?? [];
          return [[axis, {
            ...rule,
            ...(rule.fields ? { fields: rule.fields.filter(field => fields.includes(field)) } : {}),
            ...(rule.levels ? { levels: rule.levels.filter(level => level < fields.length) } : {}),
          }]];
        }),
      ),
    };
  }
  if (config.columnLevels) {
    const measures = new Set(config.values.map(value => value.prop));
    next.columnLevels = Object.fromEntries(
      Object.entries(config.columnLevels).map(([level, settings]) => [
        level,
        settings?.subtotalValues ? {
          ...settings,
          subtotalValues: settings.subtotalValues.filter(field => measures.has(field)),
        } : settings,
      ]),
    );
  }
  return next;
}

export function applyFinancialPivotOptions(
  config: PivotConfig | null,
  data: DataType[],
  configuratorVisible = false,
  configuratorMount?: HTMLElement,
  hostWidth = Infinity,
): PivotConfig | undefined {
  if (!config) return undefined;

  const rows = config.rows || [];
  const valueProps = new Set(config.values.map((value) => value.prop));
  const nextConfig: PivotConfig = {
    ...reconcileFinancialSubtotalTargets(config),
    rows,
    // Generated measure columns inherit dimension filters. Keep their filters
    // out of the headers, including Grand Total, as fields are rearranged.
    dimensions: config.dimensions?.map((dimension) => ({
      ...dimension,
      // A pinned label must leave room for an entire amount on narrow hosts.
      ...(hostWidth > 0 && hostWidth < 650 && rows.includes(dimension.prop)
        ? { size: Math.min(dimension.size ?? 180, 180, Math.floor(hostWidth / 2)) }
        : {}),
      ...(valueProps.has(dimension.prop) ? { filter: false } : {}),
    })),
    mergeValueHeaders: config.values.length === 1,
    hasConfigurator: configuratorVisible,
    mountTo: configuratorVisible ? configuratorMount : undefined,
  };

  if (typeof nextConfig.expanded === 'undefined') {
    nextConfig.expanded = getInitialExpandedGroups(filterPivotSource(data, nextConfig), rows);
  }

  return nextConfig;
}

function getInitialExpandedGroups(data: DataType[], rows: Array<string | number>) {
  const expanded: Record<string, boolean> = {};
  const groupingDepth = Math.max(0, rows.length - 1);

  data.forEach((row) => {
    const path: unknown[] = [];
    for (let index = 0; index < groupingDepth; index += 1) {
      path.push(row[rows[index]] ?? null);
      expanded[path.join(',')] = true;
    }
  });

  return expanded;
}
Interactive example

Financial Pivot

Explore synthetic finance facts with multi-measure hierarchy, filters, presets and responsive controls

The example loads when visible.

  • multiple row levels
  • generated column groups
  • sales, profit, and units-sold measures
  • product and discount-band filters
  • totals and subtotals
  • interactive configurator updates
  • pivot-config-update for external state sync
  • instant Sales Overview, Profitability, and Product Performance presets
  • responsive configuration, reset, and expanded-grid controls

Watch how the layout changes as you:

  • move fields between rows, columns, and values
  • change aggregators
  • expand or collapse grouped rows
  • enable or disable totals

The initial layout uses Country → Segment for rows and Year → Month for columns, so you can compare markets and segments while following monthly performance.

The example is useful because it shows that Pivot is not a separate reporting component. It is still RevoGrid, with generated analytical rows and columns layered on top.