Pivot Table Demo
Source code
import { defineCustomElements } from '@revolist/revogrid/loader';defineCustomElements();
import { PivotPlugin, AdvanceFilterPlugin, type PivotConfig } from '@revolist/revogrid-pro';import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';import { ECOMMERCE_PIVOT } from '../sys-data/ecommerce.pivot';import type { GroupingOptions, ColumnProp } from '@revolist/vue3-datagrid';
// ---------------------// State Management// ---------------------
let pivot: PivotConfig | null = { ...ECOMMERCE_PIVOT };let newCfg = { ...ECOMMERCE_PIVOT };let rowGroupingEnabled = false;let pivotRowsGrouping: ColumnProp[] = []; // will be updated via pivot config events
function getRowGrouping(): GroupingOptions | undefined { if (!rowGroupingEnabled || pivotRowsGrouping.length < 2) { return undefined; } // Exclude the last property const props = pivotRowsGrouping.slice(0, pivotRowsGrouping.length - 1); return { props };}
let flatHeaders: boolean = pivot ? !!pivot.flatHeaders : false;let pivotMode: boolean = !!pivot;let showConfigurator: boolean = pivot ? !!pivot.hasConfigurator : false;
// ---------------------// Render Function// ---------------------
function renderTemplate(){ return ` <div class="flex justify-between mb-3 relative" id="topBar"> <div class="flex gap-2"> <span class="flex gap-1" id="leftToggles"> <label> <input type="checkbox" id="toggleShowConfigurator" /> Show Configurator </label> <label class="ml-2"> <input type="checkbox" id="toggleFlatHeaders" /> Flat headers </label> <span class="flex ml-2 gap-1" id="rowGroupingToggleContainer"> <label> <input type="checkbox" id="toggleRowGroupingEnabled" /> Row grouping </label> </span> </span> </div> <div class="flex gap-2"> <label> <input type="checkbox" id="togglePivotMode" /> Pivot Mode </label> </div> </div> <div class="pivot-grid-container h-full overflow-hidden" id="gridContainer"></div> `;}
// ---------------------// Update Grid Function// ---------------------
function updateGrid(grid: HTMLRevoGridElement){ grid.additionalData = { pivot }; grid.grouping = getRowGrouping() || {};}
// ---------------------// Logic: Event Handlers & Initialization// ---------------------
export function load(parentSelector: string, rows: any[]){ // Create container and insert rendered HTML const container = document.createElement('div'); container.innerHTML = renderTemplate();
// Append container to parent element const parent = document.querySelector(parentSelector); if (!parent) { console.error(`Parent element "${parentSelector}" not found.`); return; } parent.appendChild(container);
// Get toggle elements const togglePivotMode = container.querySelector<HTMLInputElement>('#togglePivotMode'); const toggleShowConfigurator = container.querySelector<HTMLInputElement>('#toggleShowConfigurator'); const toggleFlatHeaders = container.querySelector<HTMLInputElement>('#toggleFlatHeaders'); const toggleRowGroupingEnabled = container.querySelector<HTMLInputElement>('#toggleRowGroupingEnabled'); const leftToggles = container.querySelector<HTMLElement>('#leftToggles');
// Initialize toggle values if (togglePivotMode) togglePivotMode.checked = pivotMode; if (toggleShowConfigurator) toggleShowConfigurator.checked = showConfigurator; if (toggleFlatHeaders) toggleFlatHeaders.checked = flatHeaders; if (toggleRowGroupingEnabled) toggleRowGroupingEnabled.checked = rowGroupingEnabled; if (leftToggles) { leftToggles.style.display = pivotMode ? 'flex' : 'none'; }
// Create and configure the grid element const gridContainer = container.querySelector<HTMLElement>('#gridContainer'); if (!gridContainer) { console.error("Grid container not found."); return; } const grid = document.createElement('revo-grid') as HTMLRevoGridElement; grid.className = "overflow-hidden skip-style h-full"; grid.filter = true; grid.source = rows; grid.columns = ECOMMERCE_COLUMNS; grid.columnTypes = ECOMMERCE_COLUMNS_TYPES; grid.additionalData = { pivot }; grid.plugins = [PivotPlugin, AdvanceFilterPlugin]; grid.grouping = getRowGrouping() || {};
// Listen for pivot configuration updates from the grid grid.addEventListener('pivot-config-update', (e: CustomEvent<PivotConfig>) => { newCfg = e.detail || { ...ECOMMERCE_PIVOT }; pivot = newCfg; pivotRowsGrouping = newCfg.rows || []; updateGrid(grid); });
gridContainer.appendChild(grid);
// Toggle event handlers if (togglePivotMode) { togglePivotMode.addEventListener('change', (e) => { pivotMode = (e.target as HTMLInputElement).checked; if (pivotMode) { pivot = newCfg; } else { pivot = null; } updateGrid(grid); if (leftToggles) { leftToggles.style.display = pivotMode ? 'flex' : 'none'; } }); } if (toggleShowConfigurator) { toggleShowConfigurator.addEventListener('change', (e) => { showConfigurator = (e.target as HTMLInputElement).checked; if (pivot) { pivot = { ...pivot, hasConfigurator: showConfigurator }; } updateGrid(grid); }); } if (toggleFlatHeaders) { toggleFlatHeaders.addEventListener('change', (e) => { flatHeaders = (e.target as HTMLInputElement).checked; if (pivot) { pivot = { ...pivot, flatHeaders: flatHeaders }; } updateGrid(grid); }); } if (toggleRowGroupingEnabled) { toggleRowGroupingEnabled.addEventListener('change', (e) => { rowGroupingEnabled = (e.target as HTMLInputElement).checked; updateGrid(grid); }); }}
import React, { useState, useMemo, useEffect, useRef } from 'react';import { RevoGrid } from '@revolist/react-datagrid';import { PivotPlugin, AdvanceFilterPlugin } from '@revolist/revogrid-pro';import { currentTheme } from '../composables/useRandomData';import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';import { ECOMMERCE_PIVOT } from '../sys-data/ecommerce.pivot';import type { PivotConfig } from '@revolist/revogrid-pro';import type { ColumnProp, DataType, GroupingOptions } from '@revolist/react-datagrid';
interface PivotProps { rows: DataType[];}interface ToggleProps { value: boolean; onChange: (value: boolean) => void;}
const Toggle: React.FC<ToggleProps> = ({ value, onChange }) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { onChange(e.target.checked); };
return ( <label className="relative inline-flex items-center cursor-pointer"> <input type="checkbox" checked={value} onChange={handleChange} className="sr-only peer" /> <div className="w-8 h-5 bg-gray-300 rounded-full peer-checked:bg-blue-500 transition-colors"></div> <div className="absolute w-4 h-4 bg-white rounded-full shadow-md left-0.5 peer-checked:translate-x-3 transition-transform"></div> </label> );};
function PivotShowcase({ rows }: PivotProps) { const { isDark } = currentTheme(); const theme = isDark() ? 'darkCompact' : 'compact';
// Pivot configuration state const [pivot, setPivot] = useState<PivotConfig | null>({ ...ECOMMERCE_PIVOT }); // A non-reactive reference to hold the latest pivot configuration const newCfgRef = useRef<PivotConfig>({ ...ECOMMERCE_PIVOT });
// Column type definitions const columnTypes = ECOMMERCE_COLUMNS_TYPES;
// Initialize plugins const plugins = useMemo(() => [PivotPlugin, AdvanceFilterPlugin], []);
// Additional data for the grid, derived from pivot config const additionalData = useMemo(() => ({ pivot }), [pivot]);
// Row grouping state const [rowGroupingEnabled, setRowGroupingEnabled] = useState(false); const [pivotRowsGrouping, setPivotRowsGrouping] = useState<ColumnProp[]>([]); const rowGrouping = useMemo<GroupingOptions | undefined>(() => { if (!rowGroupingEnabled || pivotRowsGrouping.length < 2) return undefined; // Remove the last element for grouping as per original logic const props = pivotRowsGrouping.slice(0, pivotRowsGrouping.length - 1); return { props }; }, [rowGroupingEnabled, pivotRowsGrouping]);
// When pivot config changes, update the pivotRowsGrouping state if rows exist useEffect(() => { if (pivot && pivot.rows) { setPivotRowsGrouping(pivot.rows); } }, [pivot]);
// Flat headers: getter and setter const flatHeaders = pivot?.flatHeaders || false; const onFlatHeadersChange = (value: boolean) => { if (pivot) { setPivot({ ...pivot, flatHeaders: value }); } };
// Pivot mode: determined by existence of pivot config const pivotMode = Boolean(pivot); const onPivotModeChange = (value: boolean) => { setPivot(value ? newCfgRef.current : null); };
// Show configurator toggle: getter and setter const showConfigurator = pivot?.hasConfigurator || false; const onShowConfiguratorChange = (value: boolean) => { if (pivot) { setPivot({ ...pivot, hasConfigurator: value }); } };
// Handle pivot configuration update events from the grid const configUpdate = (e: CustomEvent<PivotConfig>) => { const detail = e.detail || { ...ECOMMERCE_PIVOT }; newCfgRef.current = detail; setPivotRowsGrouping(detail.rows); };
// Attach event listener for pivot configuration updates on the grid element const gridRef = useRef<HTMLRevoGridElement>(null); useEffect(() => { const grid = gridRef.current; if (grid) { const handler = (e: Event) => { configUpdate(e as CustomEvent<PivotConfig>); }; grid.addEventListener('pivot-config-update', handler); return () => { grid.removeEventListener('pivot-config-update', handler); }; } }, []);
return ( <div> <div className="flex justify-between mb-3 relative"> <div className="flex gap-2"> {pivotMode && ( <span className="flex gap-1"> <Toggle value={showConfigurator} onChange={onShowConfiguratorChange} /> Show Configurator <Toggle value={flatHeaders} onChange={onFlatHeadersChange} /> Flat headers {!flatHeaders && ( <span className="flex ml-2 gap-1"> <Toggle value={rowGroupingEnabled} onChange={setRowGroupingEnabled} /> Row grouping </span> )} </span> )} </div> <div className="flex gap-2"> <Toggle value={pivotMode} onChange={onPivotModeChange} /> Pivot Mode </div> </div> <div className="pivot-grid-container h-full overflow-hidden"> <RevoGrid ref={gridRef} className="overflow-hidden skip-style h-full" hide-attribution range resize filter colSize={200} source={rows} columns={ECOMMERCE_COLUMNS} additionalData={additionalData} theme={theme} plugins={plugins} columnTypes={columnTypes} grouping={rowGrouping} readonly /> </div> </div> );}
export default PivotShowcase;
<template> <div class="flex justify-between mb-3 relative"> <div class="flex gap-2"> <span class="flex gap-1" v-if="pivotMode"> <Toggle v-model="showConfigurator" /> Show Configurator <Toggle class="ml-2" v-model="flatHeaders" /> Flat headers <span class="flex ml-2 gap-1" v-if="!flatHeaders" ><Toggle v-model="rowGroupingEnabled" /> Row grouping</span > </span> </div> <div class="flex gap-2"><Toggle v-model="pivotMode" /> Pivot Mode</div> </div>
<div class="pivot-grid-container h-full overflow-hidden"> <RevoGrid class="overflow-hidden skip-style h-full" hide-attribution range resize filter :colSize="200" :source="rows" :columns="ECOMMERCE_COLUMNS" :additionalData="additionalData" :theme="isDark ? 'darkCompact' : 'compact'" :plugins="plugins" :column-types="columnTypes" :grouping="rowGrouping" readonly @pivot-config-update="configUpdate" /> </div></template>
<script setup lang="ts">import { computed, ref, shallowRef, watch } from 'vue';import { currentThemeVue } from '../composables/useRandomData';const { isDark } = currentThemeVue();
import RevoGrid, { type GridPlugin, type GroupingOptions, type ColumnProp,} from '@revolist/vue3-datagrid';import { type PivotConfig, PivotPlugin, AdvanceFilterPlugin, RowSelectPlugin, groupingAggregation, commonAggregators,} from '@revolist/revogrid-pro';
import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';import { ECOMMERCE_PIVOT } from '../sys-data/ecommerce.pivot';
import Toggle from '../sys/Toggle.vue';
defineProps({ rows: { type: Array<any>, default: () => [], },});
/** * Pivot config */const pivot = shallowRef<PivotConfig | null>({ ...ECOMMERCE_PIVOT,});
/** * Grid column type properties */const columnTypes = ref(ECOMMERCE_COLUMNS_TYPES);
/** * Init plugins */const plugins: GridPlugin[] = [PivotPlugin, AdvanceFilterPlugin, RowSelectPlugin] as any[];const additionalData = computed(() => { return { pivot: pivot.value, };});
/** * Non reactive, because it updates itself */let newCfg: PivotConfig = { ...ECOMMERCE_PIVOT,};const configUpdate = (e: CustomEvent<PivotConfig>) => { newCfg = e.detail || { ...ECOMMERCE_PIVOT, }; rowGrouping.value = newCfg.rows;};
/** * Row grouping toggle */const rowGroupingEnabled = ref(false);const pivotRowsGrouping = ref<ColumnProp[]>([]);const rowGrouping = computed({ set: (v: ColumnProp[] = []) => { pivotRowsGrouping.value = v; }, get: (): GroupingOptions | undefined => { if (!rowGroupingEnabled.value || pivotRowsGrouping.value.length < 2) return undefined; const props = [...pivotRowsGrouping.value]; props.pop(); return { // this is a custom template that will be used to display the grouping aggregation groupLabelTemplate: (h, props) => groupingAggregation(h, props, { ['Age']: commonAggregators.avg, }), props, }; },});
watch( pivot, (v) => { rowGrouping.value = v?.rows; }, { immediate: true },);/** * Flat pivot column headers */const flatHeaders = computed({ get: () => pivot.value?.flatHeaders, set: (value) => { if (!pivot.value) { return; } pivot.value = { ...pivot.value, flatHeaders: value, }; },});/** * Pivot mode disable toggle */const pivotMode = computed({ get: () => !!pivot.value, set: (value) => { pivot.value = value ? newCfg : null; },});
/** * Show configurator toggle */const showConfigurator = computed({ get: () => pivot.value?.hasConfigurator, set: (value) => { if (!pivot.value) { return; } pivot.value = { ...pivot.value, hasConfigurator: value, }; },});
</script>
// @ts-ignoreimport { Component, ViewEncapsulation, Input, OnInit } from '@angular/core';import { RevoGrid, type ColumnProp, type GroupingOptions } from '@revolist/angular-datagrid';import NumberColumnType from '@revolist/revogrid-column-numeral';import { PivotPlugin, AdvanceFilterPlugin, type PivotConfig,} from '@revolist/revogrid-pro';import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';import { ECOMMERCE_PIVOT } from '../sys-data/ecommerce.pivot';
@Component({ selector: 'pivot-grid', standalone: true, imports: [RevoGrid], template: ` <div class="flex justify-between mb-3 relative"> <div class="flex gap-2"> <span class="flex gap-1" *ngIf="pivotMode"> <input type="checkbox" [(ngModel)]="showConfigurator" /> Show Configurator <input type="checkbox" class="ml-2" [(ngModel)]="flatHeaders" /> Flat headers <span class="flex ml-2 gap-1" *ngIf="!flatHeaders"> <input type="checkbox" [(ngModel)]="rowGroupingEnabled" /> Row grouping </span> </span> </div> <div class="flex gap-2"> <input type="checkbox" [(ngModel)]="pivotMode" /> Pivot Mode </div> </div>
<div class="pivot-grid-container h-full overflow-hidden"> <revo-grid class="overflow-hidden skip-style h-full" theme="compact" [hideAttribution]="true" [range]="true" [resize]="true" [filter]="true" [colSize]="200" [source]="rows" [columns]="ECOMMERCE_COLUMNS" [additionalData]="additionalData" [plugins]="plugins" [columnTypes]="columnTypes" [grouping]="rowGrouping" [readonly]="true" (pivot-config-update)="configUpdate($event)" ></revo-grid> </div> `, encapsulation: ViewEncapsulation.None,})export class PivotGridComponent implements OnInit { @Input() rows: any[] = [];
ECOMMERCE_COLUMNS = ECOMMERCE_COLUMNS;
pivot: PivotConfig | null = { ...ECOMMERCE_PIVOT };
columnTypes = ECOMMERCE_COLUMNS_TYPES;
plugins = [PivotPlugin, AdvanceFilterPlugin];
additionalData = { pivot: this.pivot, };
rowGroupingEnabled = false; pivotRowsGrouping: ColumnProp[] = [];
get rowGrouping(): GroupingOptions | undefined { if (!this.rowGroupingEnabled || this.pivotRowsGrouping.length < 2) return undefined; const props = [...this.pivotRowsGrouping]; props.pop(); return { props }; }
set rowGrouping(value: ColumnProp[]) { this.pivotRowsGrouping = value; }
configUpdate(event: CustomEvent<PivotConfig>) { const newCfg = event.detail || { ...ECOMMERCE_PIVOT }; this.pivot = newCfg; this.rowGrouping = newCfg.rows || []; }
get flatHeaders(): boolean { return this.pivot?.flatHeaders ?? false; }
set flatHeaders(value: boolean) { if (!this.pivot) return; this.pivot = { ...this.pivot, flatHeaders: value }; }
get pivotMode(): boolean { return !!this.pivot; }
set pivotMode(value: boolean) { this.pivot = value ? { ...ECOMMERCE_PIVOT } : null; }
get showConfigurator(): boolean { return this.pivot?.hasConfigurator ?? false; }
set showConfigurator(value: boolean) { if (!this.pivot) return; this.pivot = { ...this.pivot, hasConfigurator: value }; }
ngOnInit() { // Sync grouping on initialization this.rowGrouping = this.pivot?.rows || []; }}
import { type PivotConfigDimension, type PivotConfig, commonAggregators, ratingStarRenderer, progressLineWithValueRenderer, changeRenderer,} from '@revolist/revogrid-pro';
export const ECOMMERCE_PIVOT: PivotConfig = { dimensions: [ { prop: 'Age', columnType: 'integer', size: 100, sortable: true, }, { prop: 'City', sortable: true, rowSelect: true, filter: ['string', 'selection'], order: 'asc', }, { prop: 'Gender', filter: ['string', 'selection'], sortable: true, }, { prop: 'Membership Type', filter: ['string', 'selection'], sortable: true, }, { prop: 'Total Spend', sortable: true, columnType: 'currency', filter: ['number'], cellProperties: ({ value }) => ({ class: { highlight: value > 20000, 'align-right': true, }, style: { backgroundColor: value > 20000 ? 'rgb(255, 211, 35)' : '', color: value > 20000 ? '#000' : '', }, }), aggregators: { sum: commonAggregators.sum, avg: commonAggregators.avg, min: commonAggregators.min, max: commonAggregators.max, }, }, { name: 'Spend Change %', prop: 'Spend Change (%)', sortable: true, filter: ['number'], columnType: 'percent', cellTemplate: changeRenderer, }, { name: 'Avg Rating', prop: 'Average Rating', filter: ['number', 'slider'], sortable: true, maxStars: 5, maxValue: 5, thresholds: [ { value: 4, className: 'high' }, { value: 3, className: 'medium' }, { value: 2, className: 'low' }, ], cellParser: (model, column) => { const value = model[column.prop]; if (Number(value)) { return value.toFixed(2); } return value; }, cellTemplate(...args) { const column: PivotConfigDimension = args[1].column; if (column.dimension === 'values') { switch (column.aggregator) { case 'star': return ratingStarRenderer(...args); case 'prg': return progressLineWithValueRenderer(...args); } } return args[1].value; }, aggregators: { prg: commonAggregators.avg, star: commonAggregators.avg, }, }, { prop: 'Discount Applied', sortable: true, filter: ['selection'], }, ], rows: ['City', 'Age'], columns: ['Gender'], values: [ { prop: 'Total Spend', aggregator: 'sum', }, { prop: 'Average Rating', aggregator: 'prg', }, ], hasConfigurator: true, flatHeaders: false,};