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
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import { filterPivotSource, type PivotConfig } from '@revolist/revogrid-enterprise';
import './financial-pivot-header.scss';
import { currentTheme } from '../composables/useRandomData';
import {
  FINANCIAL_COLUMNS,
  FINANCIAL_COLUMN_TYPES,
  FINANCIAL_MULTI_ROW_HEADER,
  FINANCIAL_SHOWCASE_PLUGINS,
  applyFinancialPivotOptions,
  createFinancialPreset,
  getFinancialKpis,
  resolveFinancialRows,
  type FinancialPresetId,
} from './financial.pivot';
import {
  FINANCIAL_PIVOT_CONFIGURATOR_EVENT,
  FINANCIAL_PIVOT_EXPANDED_EVENT,
  FINANCIAL_PIVOT_PRESET_EVENT,
  FINANCIAL_PIVOT_RESET_EVENT,
  createFinancialPivotHeader,
} from './financial-pivot-header';
import {
  createFinancialPivotGuidance,
} from './financial-pivot-guidance';

defineCustomElements();

const isSmallScreen = () => window.matchMedia('(max-width: 767px)').matches;

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

  const { isDark } = currentTheme();
  const data = resolveFinancialRows(Array.isArray(rows) ? rows : undefined);
  let pivotConfig: PivotConfig = createFinancialPreset();
  let activePreset: FinancialPresetId = 'sales';
  let configuratorVisible = !isSmallScreen();
  let expanded = false;

  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,
    kpis: getFinancialKpis(
      filterPivotSource(data, pivotConfig),
      activePreset,
    ),
  });

  const guidance = createFinancialPivotGuidance(pivotConfig);

  const scrollContainer = document.createElement('div');
  scrollContainer.className = 'grow min-h-0 overflow-auto';
  const gridContainer = document.createElement('div');
  gridContainer.className = 'pivot-grid-container h-full overflow-hidden';
  let grid: any;
  const getFilteredData = () => filterPivotSource(data, pivotConfig);

  function applyPivotOptions() {
    grid.pivot = applyFinancialPivotOptions(
      pivotConfig,
      data,
      configuratorVisible,
    );
  }

  function refreshLayout() {
    header.state = {
      activePreset,
      configuratorVisible,
      expanded,
      kpis: getFinancialKpis(
        getFilteredData(),
        activePreset,
      ),
    };
    guidance.config = pivotConfig;
    gridContainer.style.minWidth = configuratorVisible ? '920px' : '680px';
    Object.assign(container.style, expanded
      ? { position: 'fixed', inset: '8px', zIndex: '1000', background: 'var(--financial-pivot-expanded-background)' }
      : { position: '', inset: '', zIndex: '', background: '' });
  }

  const resetDemo = () => {
    pivotConfig = createFinancialPreset();
    activePreset = 'sales';
    configuratorVisible = !isSmallScreen();
    expanded = false;
    guidance.visible = true;
    refreshLayout();
    replacePivotOptions();
  };

  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: 180,
      readonly: true,
      theme: isDark() ? 'darkCompact' : 'compact',
      columns: FINANCIAL_COLUMNS,
      columnTypes: FINANCIAL_COLUMN_TYPES,
      plugins: FINANCIAL_SHOWCASE_PLUGINS,
      pivot: applyFinancialPivotOptions(
        pivotConfig,
        data,
        configuratorVisible,
      ),
    });
    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, () => {
    configuratorVisible = !configuratorVisible;
    applyPivotOptions();
    refreshLayout();
  });
  header.addEventListener(FINANCIAL_PIVOT_EXPANDED_EVENT, () => {
    expanded = !expanded;
    refreshLayout();
  });
  header.addEventListener(FINANCIAL_PIVOT_RESET_EVENT, resetDemo);
  scrollContainer.appendChild(gridContainer);
  container.append(header, guidance, scrollContainer);
  parent.appendChild(container);
  refreshLayout();
  initializeGrid();

  return () => {
    grid.removeEventListener('pivot-config-update', onPivotConfigUpdate as EventListener);
    grid.remove();
    container.remove();
  };
}
React tsx
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type DetailedHTMLProps,
  type HTMLAttributes,
} from 'react';
import { RevoGrid, type DataType } from '@revolist/react-datagrid';
import { filterPivotSource, type PivotConfig } from '@revolist/revogrid-enterprise';
import './financial-pivot-header.scss';
import { currentTheme } from '../composables/useRandomData';
import {
  FINANCIAL_COLUMNS,
  FINANCIAL_COLUMN_TYPES,
  FINANCIAL_MULTI_ROW_HEADER,
  FINANCIAL_SHOWCASE_PLUGINS,
  applyFinancialPivotOptions,
  createFinancialPreset,
  getFinancialKpis,
  resolveFinancialRows,
  type FinancialPresetId,
} from './financial.pivot';
import {
  FINANCIAL_PIVOT_CONFIGURATOR_EVENT,
  FINANCIAL_PIVOT_EXPANDED_EVENT,
  FINANCIAL_PIVOT_PRESET_EVENT,
  FINANCIAL_PIVOT_RESET_EVENT,
  defineFinancialPivotHeaderElement,
  type FinancialPivotHeaderElement,
  type FinancialPivotHeaderState,
} from './financial-pivot-header';
import {
  FINANCIAL_PIVOT_GUIDANCE_DISMISS_EVENT,
  defineFinancialPivotGuidanceElement,
  type FinancialPivotGuidanceElement,
} from './financial-pivot-guidance';

defineFinancialPivotHeaderElement();
defineFinancialPivotGuidanceElement();

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

interface PivotProps {
  rows?: DataType[];
}

const isSmallScreen = () => typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;

function PivotShowcase({ rows }: PivotProps) {
  const { isDark } = currentTheme();
  const data = useMemo(() => resolveFinancialRows(rows), [rows]);
  const [pivotConfig, setPivotConfig] = useState<PivotConfig>(() => createFinancialPreset());
  const [activePreset, setActivePreset] = useState<FinancialPresetId>('sales');
  const [configuratorVisible, setConfiguratorVisible] = useState(() => !isSmallScreen());
  const [guidanceVisible, setGuidanceVisible] = useState(true);
  const [expanded, setExpanded] = useState(false);
  const filteredData = useMemo(
    () => filterPivotSource(data, pivotConfig),
    [data, pivotConfig],
  );

  const pivot = useMemo(
    () => applyFinancialPivotOptions(pivotConfig, data, configuratorVisible),
    [pivotConfig, data, configuratorVisible],
  );
  const plugins = useMemo(() => FINANCIAL_SHOWCASE_PLUGINS, []);
  const columns = useMemo(() => FINANCIAL_COLUMNS, []);
  const columnTypes = useMemo(() => FINANCIAL_COLUMN_TYPES, []);
  const kpis = useMemo(
    () => getFinancialKpis(
      filteredData,
      activePreset,
    ),
    [activePreset, filteredData],
  );
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const headerRef = useRef<FinancialPivotHeaderElement>(null);
  const guidanceRef = useRef<FinancialPivotGuidanceElement>(null);

  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 resetDemo = useCallback(() => {
    if (gridRef.current) gridRef.current.pivot = undefined;
    window.setTimeout(() => {
      setPivotConfig(createFinancialPreset());
      setActivePreset('sales');
    });
    setConfiguratorVisible(!isSmallScreen());
    setGuidanceVisible(true);
    setExpanded(false);
  }, []);

  const headerState = useMemo<FinancialPivotHeaderState>(() => ({
    activePreset,
    configuratorVisible,
    expanded,
    kpis,
  }), [activePreset, configuratorVisible, expanded, kpis]);

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

  useEffect(() => {
    if (guidanceRef.current) {
      guidanceRef.current.visible = guidanceVisible;
      guidanceRef.current.config = pivotConfig;
    }
  }, [guidanceVisible, pivotConfig]);

  useEffect(() => {
    const guidance = guidanceRef.current;
    if (!guidance) return;
    const onDismiss = () => setGuidanceVisible(false);
    guidance.addEventListener(FINANCIAL_PIVOT_GUIDANCE_DISMISS_EVENT, onDismiss);
    return () => guidance.removeEventListener(FINANCIAL_PIVOT_GUIDANCE_DISMISS_EVENT, onDismiss);
  }, []);

  useEffect(() => {
    const header = headerRef.current;
    if (!header) return;
    const onPreset = (event: Event) => {
      selectPreset((event as CustomEvent<FinancialPresetId>).detail);
    };
    const onConfigurator = () => setConfiguratorVisible((value) => !value);
    const onExpanded = () => setExpanded((value) => !value);
    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_RESET_EVENT, resetDemo);
    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_RESET_EVENT, resetDemo);
    };
  }, [resetDemo, selectPreset]);

  return (
    <div
      className="financial-pivot-showcase grow flex flex-col gap-2 h-full p-2 box-border"
      style={expanded ? { position: 'fixed', inset: 8, zIndex: 1000, background: 'var(--financial-pivot-expanded-background)' } : undefined}
    >
      <financial-pivot-header ref={headerRef} />

      <financial-pivot-guidance ref={guidanceRef} />

      <div className="grow min-h-0 overflow-auto">
        <div className="pivot-grid-container h-full overflow-hidden" style={{ minWidth: configuratorVisible ? 920 : 680 }}>
          <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={180}
            source={data}
            columns={columns}
            pivot={pivot}
            theme={isDark() ? 'darkCompact' : 'compact'}
            plugins={plugins}
            columnTypes={columnTypes}
            readonly
          />
        </div>
      </div>
    </div>
  );
}

export default PivotShowcase;
Vue vue
<template>
  <div
    class="financial-pivot-showcase grow flex flex-col gap-2 h-full p-2 box-border"
    :style="expandedStyle"
  >
    <financial-pivot-header
      :state.prop="headerState"
      @financial-pivot-preset-select="onPresetSelect"
      @financial-pivot-configurator-toggle="configuratorVisible = !configuratorVisible"
      @financial-pivot-expanded-toggle="expanded = !expanded"
      @financial-pivot-reset="resetDemo"
    />

    <financial-pivot-guidance
      :visible.prop="guidanceVisible"
      :config.prop="pivotConfig"
      @financial-pivot-guidance-dismiss="guidanceVisible = false"
    />

    <div class="grow min-h-0 overflow-auto">
      <div
        class="pivot-grid-container h-full overflow-hidden"
        :style="{ minWidth: configuratorVisible ? '920px' : '680px' }"
      >
        <RevoGrid
          ref="gridElement"
          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="180"
          :source="rows"
          :columns="FINANCIAL_COLUMNS"
          :pivot.prop="pivot"
          :theme="isDark ? 'darkCompact' : 'compact'"
          :plugins="plugins"
          :column-types="columnTypes"
          readonly
          @pivot-config-update="configUpdate"
        />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { filterPivotSource, type PivotConfig } from '@revolist/revogrid-enterprise';
import { currentThemeVue } from '../composables/useRandomData';
import {
  FINANCIAL_COLUMNS,
  FINANCIAL_COLUMN_TYPES,
  FINANCIAL_MULTI_ROW_HEADER,
  FINANCIAL_SHOWCASE_PLUGINS,
  applyFinancialPivotOptions,
  createFinancialPreset,
  getFinancialKpis,
  resolveFinancialRows,
  type FinancialPresetId,
} from './financial.pivot';
import {
  defineFinancialPivotHeaderElement,
  type FinancialPivotHeaderState,
} from './financial-pivot-header';
import { defineFinancialPivotGuidanceElement } from './financial-pivot-guidance';

defineFinancialPivotHeaderElement();
defineFinancialPivotGuidanceElement();

const { isDark } = currentThemeVue();
const props = defineProps({ rows: { type: Array<any>, default: () => [] } });
const rows = ref(resolveFinancialRows(props.rows));
const isSmallScreen = () => typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;

const pivotConfig = shallowRef<PivotConfig>(createFinancialPreset());
const activePreset = ref<FinancialPresetId>('sales');
const configuratorVisible = ref(!isSmallScreen());
const guidanceVisible = ref(true);
const expanded = ref(false);
const gridElement = ref<HTMLRevoGridElement>();
const columnTypes = ref(FINANCIAL_COLUMN_TYPES);
const plugins = FINANCIAL_SHOWCASE_PLUGINS;
const filteredRows = computed(() => filterPivotSource(rows.value, pivotConfig.value));
const kpis = computed(() => getFinancialKpis(
  filteredRows.value,
  activePreset.value,
));
const headerState = computed<FinancialPivotHeaderState>(() => ({
  activePreset: activePreset.value,
  configuratorVisible: configuratorVisible.value,
  expanded: expanded.value,
  kpis: kpis.value,
}));

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

const expandedStyle = computed(() => expanded.value
  ? { position: 'fixed', inset: '8px', zIndex: 1000, background: 'var(--financial-pivot-expanded-background)' }
  : undefined,
);

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 resetDemo = () => {
  if (gridElement.value) gridElement.value.pivot = undefined;
  window.setTimeout(() => {
    pivotConfig.value = createFinancialPreset();
    activePreset.value = 'sales';
  });
  configuratorVisible.value = !isSmallScreen();
  guidanceVisible.value = true;
  expanded.value = false;
};
</script>

<style lang="scss">
@use './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>
Angular ts
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  ElementRef,
  Input,
  NO_ERRORS_SCHEMA,
  signal,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { RevoGrid, type DataType } from '@revolist/angular-datagrid';
import { filterPivotSource, type PivotConfig } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';
import {
  FINANCIAL_COLUMNS,
  FINANCIAL_COLUMN_TYPES,
  FINANCIAL_MULTI_ROW_HEADER,
  FINANCIAL_SHOWCASE_PLUGINS,
  applyFinancialPivotOptions,
  createFinancialPreset,
  getFinancialKpis,
  resolveFinancialRows,
  type FinancialPresetId,
} from './financial.pivot';
import {
  defineFinancialPivotHeaderElement,
  type FinancialPivotHeaderState,
} from './financial-pivot-header';
import { defineFinancialPivotGuidanceElement } from './financial-pivot-guidance';

defineFinancialPivotHeaderElement();
defineFinancialPivotGuidanceElement();

const isSmallScreen = () => typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;

@Component({
  selector: 'pivot-showcase-grid',
  standalone: true,
  imports: [RevoGrid, CommonModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  styleUrls: ['./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"
      [style.position]="expanded() ? 'fixed' : null"
      [style.inset]="expanded() ? '8px' : null"
      [style.z-index]="expanded() ? 1000 : null"
      [style.background]="expanded() ? 'var(--financial-pivot-expanded-background)' : null"
    >
      <financial-pivot-header
        [state]="headerState"
        (financial-pivot-preset-select)="selectPreset($any($event).detail)"
        (financial-pivot-configurator-toggle)="configuratorVisible.set(!configuratorVisible())"
        (financial-pivot-expanded-toggle)="expanded.set(!expanded())"
        (financial-pivot-reset)="resetDemo()"
      ></financial-pivot-header>

      <financial-pivot-guidance
        [visible]="guidanceVisible()"
        [config]="pivotSignal()"
        (financial-pivot-guidance-dismiss)="guidanceVisible.set(false)"
      ></financial-pivot-guidance>

      <div class="grow min-h-0 overflow-auto">
        <div
          class="pivot-grid-container h-full overflow-hidden"
          [style.min-width]="configuratorVisible() ? '920px' : '680px'"
        >
          <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]="180"
            [source]="rows"
            [columns]="FINANCIAL_COLUMNS"
            [pivot]="pivot()"
            [theme]="theme"
            [plugins]="plugins"
            [columnTypes]="columnTypes"
            [readonly]="true"
            (pivot-config-update)="configUpdate($event)"
          ></revo-grid>
        </div>
      </div>
    </div>
  `,
})
export class PivotShowcaseGridComponent {
  @Input() rows: DataType[] = resolveFinancialRows();
  @ViewChild('gridElement', { read: ElementRef })
  gridElement?: ElementRef<HTMLRevoGridElement & { pivot?: PivotConfig }>;

  readonly FINANCIAL_COLUMNS = FINANCIAL_COLUMNS;
  readonly columnTypes = FINANCIAL_COLUMN_TYPES;
  readonly multiRowHeader = FINANCIAL_MULTI_ROW_HEADER;
  readonly plugins = FINANCIAL_SHOWCASE_PLUGINS;
  readonly theme = currentTheme().isDark() ? 'darkCompact' : 'compact';

  readonly pivotSignal = signal<PivotConfig>(createFinancialPreset());
  readonly activePreset = signal<FinancialPresetId>('sales');
  readonly configuratorVisible = signal(!isSmallScreen());
  readonly guidanceVisible = signal(true);
  readonly expanded = signal(false);

  get headerState(): FinancialPivotHeaderState {
    return {
      activePreset: this.activePreset(),
      configuratorVisible: this.configuratorVisible(),
      expanded: this.expanded(),
      kpis: getFinancialKpis(
        this.filteredRows(),
        this.activePreset(),
      ),
    };
  }

  readonly filteredRows = computed(() => filterPivotSource(
    this.rows as ReturnType<typeof resolveFinancialRows>,
    this.pivotSignal(),
  ));

  readonly pivot = computed(() => applyFinancialPivotOptions(
    this.pivotSignal(),
    this.rows,
    this.configuratorVisible(),
  ));

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

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

  resetDemo() {
    this.replacePivot(createFinancialPreset(), 'sales');
    this.configuratorVisible.set(!isSmallScreen());
    this.guidanceVisible.set(true);
    this.expanded.set(false);
  }

  private replacePivot(config: PivotConfig, preset: FinancialPresetId) {
    if (this.gridElement) this.gridElement.nativeElement.pivot = undefined;
    window.setTimeout(() => {
      this.pivotSignal.set(config);
      this.activePreset.set(preset);
    });
  }
}
Pivot Config ts
import type { ColumnRegular, DataType, GridPlugin } from '@revolist/revogrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
  type PivotConfig,
  type PivotConfigDimension,
  filterPivotSource,
  PivotPlugin,
} from '@revolist/revogrid-enterprise';
import {
  AdvanceFilterPlugin,
  ColumnCollapsePlugin,
  FilterHeaderPlugin,
  MultiRowHeaderPlugin,
  RowOddPlugin,
  RowSelectPlugin,
  SameValueMergePlugin,
  commonAggregators,
} from '@revolist/revogrid-pro';
import chartLineIcon from '@fortawesome/fontawesome-free/svgs/solid/chart-line.svg?raw';
import coinsIcon from '@fortawesome/fontawesome-free/svgs/solid/coins.svg?raw';
import percentIcon from '@fortawesome/fontawesome-free/svgs/solid/percent.svg?raw';
import boxesStackedIcon from '@fortawesome/fontawesome-free/svgs/solid/boxes-stacked.svg?raw';
import { FINANCIAL_DATA, type FinancialRow } from './financial-dataset';
import { createFinancialHeatmapColumnType } from './financial.heatmap';

export const FINANCIAL_COLUMN_TYPES = {
  currency: new NumberColumnType('$0,0.00'),
  number: new NumberColumnType('0,0.00'),
  integer: new NumberColumnType('0,0'),
  salesHeatmap: createFinancialHeatmapColumnType('Sales', 'currency'),
  profitHeatmap: createFinancialHeatmapColumnType('Profit', 'currency'),
  unitsHeatmap: createFinancialHeatmapColumnType('Units Sold', 'number'),
  grossSalesHeatmap: createFinancialHeatmapColumnType('Gross Sales', 'currency'),
  discountsHeatmap: createFinancialHeatmapColumnType('Discounts', 'currency'),
  cogsHeatmap: createFinancialHeatmapColumnType('COGS', 'currency'),
};

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

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));

export const FINANCIAL_DIMENSIONS: PivotConfigDimension[] = [
  { prop: 'Country', sortable: true, order: 'asc', merge: true, filter: ['string', 'selection'] },
  { prop: 'Segment', sortable: true, order: 'asc', merge: true, filter: ['string', 'selection'] },
  { prop: 'Year', sortable: true, order: 'asc', columnType: 'integer', filter: ['number', 'selection'] },
  {
    prop: 'Month',
    sortable: true,
    order: 'asc',
    cellCompare: monthCompare,
    filter: ['string', 'selection'],
    filterOptions: [...MONTH_ORDER],
  },
  { prop: 'Product', sortable: true, filter: ['string', 'selection'] },
  {
    prop: 'Discount Band',
    sortable: true,
    filter: ['string', 'selection'],
    filterOptions: ['None', 'Low', 'Medium', 'High'],
  },
  {
    prop: 'Units Sold',
    sortable: true,
    columnType: 'unitsHeatmap',
    filter: ['number'],
    aggregators: numberAggregators,
  },
  {
    prop: 'Sales',
    sortable: true,
    columnType: 'salesHeatmap',
    filter: ['number'],
    aggregators: currencyAggregators,
  },
  {
    prop: 'Profit',
    sortable: true,
    columnType: 'profitHeatmap',
    filter: ['number'],
    aggregators: currencyAggregators,
  },
  {
    prop: 'Gross Sales',
    sortable: true,
    columnType: 'grossSalesHeatmap',
    filter: ['number'],
    aggregators: currencyAggregators,
  },
  {
    prop: 'Discounts',
    sortable: true,
    columnType: 'discountsHeatmap',
    filter: ['number'],
    aggregators: currencyAggregators,
  },
  {
    prop: 'COGS',
    sortable: true,
    columnType: 'cogsHeatmap',
    filter: ['number'],
    aggregators: currencyAggregators,
  },
  { prop: 'Date', sortable: true, filter: ['string'] },
];

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

export const FINANCIAL_SHOWCASE_PLUGINS: GridPlugin[] = [
  FilterHeaderPlugin,
  RowSelectPlugin,
  SameValueMergePlugin,
  PivotPlugin,
  ColumnCollapsePlugin,
  MultiRowHeaderPlugin,
  AdvanceFilterPlugin,
  RowOddPlugin,
] as GridPlugin[];

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', 'Segment'],
  columns: ['Year', 'Month'],
  values: [
    { prop: 'Sales', aggregator: 'sum' },
    { prop: 'Profit', aggregator: 'sum' },
    { prop: 'Units Sold', aggregator: 'sum' },
  ],
  filters: ['Product', 'Discount Band'],
  filterSelections: { 'Discount Band': ['High'] },
  hasConfigurator: true,
  flatHeaders: false,
  collapsed: true,
  groupAggregations: true,
  columnCollapse: {
    enabled: true,
    collapsed: false,
    aggregator: {
      Sales: 'sum',
      Profit: 'sum',
      'Units Sold': 'sum',
    },
    placeholder: 'Period Total',
  },
  totals: {
    subtotals: true,
    grandTotal: true,
    subtotalLabel: 'Subtotal',
    grandTotalLabel: 'Grand Total',
  },
};

const PROFITABILITY: PivotConfig = {
  ...SALES_OVERVIEW,
  rows: ['Segment', 'Country'],
  columns: ['Year', 'Month'],
  values: [
    { prop: 'Profit', aggregator: 'sum' },
    { prop: 'Sales', aggregator: 'sum' },
    { prop: 'COGS', aggregator: 'sum' },
  ],
  filters: ['Product', 'Discount Band'],
  filterSelections: { 'Discount Band': ['Medium'] },
  columnCollapse: {
    enabled: true,
    collapsed: false,
    aggregator: { Profit: 'sum', Sales: 'sum', COGS: 'sum' },
    placeholder: 'Period Total',
  },
};

const PRODUCT_PERFORMANCE: PivotConfig = {
  ...SALES_OVERVIEW,
  rows: ['Product', 'Segment'],
  columns: ['Year', 'Country'],
  values: [
    { prop: 'Gross Sales', aggregator: 'sum' },
    { prop: 'Units Sold', aggregator: 'sum' },
    { prop: 'Discounts', aggregator: 'sum' },
  ],
  filters: ['Month', 'Discount Band'],
  filterSelections: { Month: ['December'] },
  columnCollapse: {
    enabled: true,
    collapsed: false,
    aggregator: { 'Gross Sales': 'sum', 'Units Sold': 'sum', Discounts: 'sum' },
    placeholder: 'Market Total',
  },
};

export type FinancialPresetId = 'sales' | 'profitability' | 'product';

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

export const FINANCIAL_PRESETS: FinancialPreset[] = [
  {
    id: 'sales',
    label: 'Sales Overview',
    description: 'Compare monthly revenue and demand across markets and segments.',
  },
  {
    id: 'profitability',
    label: 'Profitability',
    description: 'See which segments convert revenue into profit most effectively.',
  },
  {
    id: 'product',
    label: 'Product Performance',
    description: 'Understand which products drive volume, gross sales, and discounts.',
  },
];

export interface FinancialKpi {
  label: string;
  value: string;
  detail: string;
  icon: string;
  tone: 'sales' | 'profit' | 'margin' | 'units';
}

function prepareKpiIcon(svg: string) {
  return svg.replace(
    '<svg ',
    '<svg width="20" height="20" fill="currentColor" aria-hidden="true" focusable="false" ',
  );
}

const compactCurrency = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  notation: 'compact',
  minimumFractionDigits: 1,
  maximumFractionDigits: 1,
});

const compactNumber = new Intl.NumberFormat('en-US', {
  notation: 'compact',
  minimumFractionDigits: 1,
  maximumFractionDigits: 1,
});

const percentNumber = new Intl.NumberFormat('en-US', {
  style: 'percent',
  minimumFractionDigits: 1,
  maximumFractionDigits: 1,
});


export function getFinancialKpis(
  rows: FinancialRow[],
  preset: FinancialPresetId = 'sales',
): FinancialKpi[] {
  const summary = rows.reduce(
    (result, row) => ({
      sales: result.sales + row.Sales,
      profit: result.profit + row.Profit,
      units: result.units + row['Units Sold'],
      grossSales: result.grossSales + row['Gross Sales'],
      discounts: result.discounts + row.Discounts,
      cogs: result.cogs + row.COGS,
    }),
    { sales: 0, profit: 0, units: 0, grossSales: 0, discounts: 0, cogs: 0 },
  );
  const years = new Set(rows.map((row) => row.Year));
  const markets = new Set(rows.map((row) => row.Country));
  const products = new Set(rows.map((row) => row.Product));

  if (preset === 'sales') {
    return [
      {
        label: 'Net Sales',
        value: compactCurrency.format(summary.sales),
        detail: `${rows.length.toLocaleString('en-US')} transactions`,
        icon: prepareKpiIcon(chartLineIcon),
        tone: 'sales',
      },
      {
        label: 'Profit',
        value: compactCurrency.format(summary.profit),
        detail: `${years.size} fiscal years`,
        icon: prepareKpiIcon(coinsIcon),
        tone: 'profit',
      },
      {
        label: 'Profit Margin',
        value: percentNumber.format(summary.sales ? summary.profit / summary.sales : 0),
        detail: 'Profit ÷ net sales',
        icon: prepareKpiIcon(percentIcon),
        tone: 'margin',
      },
      {
        label: 'Units Sold',
        value: compactNumber.format(summary.units),
        detail: `${markets.size} global markets`,
        icon: prepareKpiIcon(boxesStackedIcon),
        tone: 'units',
      },
    ];
  }

  if (preset === 'profitability') {
    return [
      {
        label: 'Profit',
        value: compactCurrency.format(summary.profit),
        detail: `${years.size} fiscal years`,
        icon: prepareKpiIcon(coinsIcon),
        tone: 'profit',
      },
      {
        label: 'Profit Margin',
        value: percentNumber.format(summary.sales ? summary.profit / summary.sales : 0),
        detail: 'Profit ÷ net sales',
        icon: prepareKpiIcon(percentIcon),
        tone: 'margin',
      },
      {
        label: 'COGS',
        value: compactCurrency.format(summary.cogs),
        detail: `${markets.size} global markets`,
        icon: prepareKpiIcon(boxesStackedIcon),
        tone: 'units',
      },
      {
        label: 'Gross Margin',
        value: percentNumber.format(
          summary.grossSales ? (summary.grossSales - summary.cogs) / summary.grossSales : 0,
        ),
        detail: 'Gross sales less COGS',
        icon: prepareKpiIcon(chartLineIcon),
        tone: 'sales',
      },
    ];
  }

  return [
    {
      label: 'Gross Sales',
      value: compactCurrency.format(summary.grossSales),
      detail: `${rows.length.toLocaleString('en-US')} transactions`,
      icon: prepareKpiIcon(chartLineIcon),
      tone: 'sales',
    },
    {
      label: 'Units Sold',
      value: compactNumber.format(summary.units),
      detail: `${products.size} products`,
      icon: prepareKpiIcon(boxesStackedIcon),
      tone: 'units',
    },
    {
      label: 'Discounts',
      value: compactCurrency.format(summary.discounts),
      detail: 'Gross-to-net reductions',
      icon: prepareKpiIcon(coinsIcon),
      tone: 'profit',
    },
    {
      label: 'Discount Rate',
      value: percentNumber.format(
        summary.grossSales ? summary.discounts / summary.grossSales : 0,
      ),
      detail: 'Discounts ÷ gross sales',
      icon: prepareKpiIcon(percentIcon),
      tone: 'margin',
    },
  ];
}

export function resolveFinancialKpiPreset(config: PivotConfig): FinancialPresetId {
  const measures = new Set(config.values.map(({ prop }) => String(prop)));
  if (measures.has('Gross Sales') || measures.has('Discounts')) return 'product';
  if (measures.has('COGS')) return 'profitability';
  return 'sales';
}

const PRESET_CONFIGS: Record<FinancialPresetId, PivotConfig> = {
  sales: SALES_OVERVIEW,
  profitability: PROFITABILITY,
  product: PRODUCT_PERFORMANCE,
};

export function createFinancialPreset(id: FinancialPresetId = 'sales'): PivotConfig {
  const config = PRESET_CONFIGS[id];
  return {
    ...config,
    dimensions: config.dimensions?.map((dimension) => ({ ...dimension })),
    rows: [...config.rows],
    columns: [...(config.columns || [])],
    values: config.values.map((value) => ({ ...value })),
    filters: [...(config.filters || [])],
    filterSelections: Object.fromEntries(
      Object.entries(config.filterSelections || {}).map(([prop, selection]) => [
        prop,
        [...(selection || [])],
      ]),
    ),
    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();

export function resolveFinancialRows(rows?: DataType[]): FinancialRow[] {
  return rows?.length ? rows as FinancialRow[] : FINANCIAL_DATA;
}

export function applyFinancialPivotOptions(
  config: PivotConfig | null,
  data: DataType[],
  configuratorVisible = true,
): PivotConfig | undefined {
  if (!config) return undefined;

  const rows = config.rows || [];
  const nextConfig: PivotConfig = {
    ...config,
    rows,
    hasConfigurator: configuratorVisible,
  };

  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;
}
  • 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.