Skip to content

Grouping with Aggregation

The Grouping with Aggregation feature allows you to organize your data into hierarchical groups and display custom aggregated values for each group. This is particularly useful for displaying summary statistics, totals, averages, or custom calculations for grouped data.

  • Hierarchical Grouping: Group data by multiple columns in a tree-like structure
  • Custom Aggregations: Define custom aggregation functions for different data types
  • Visual Indicators: Expandable/collapsible groups with aggregation values displayed
  • Flexible Templates: Customize how group headers and aggregations are displayed
Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import { currentTheme } from '../composables/useRandomData';
import { createGroupingData, columns, grouping } from './groupingData';

defineCustomElements();

export function load(parentSelector: string) {
  const grid = document.createElement('revo-grid');
  const { isDark } = currentTheme();

  // Generate realistic data using faker.js
  grid.source = createGroupingData(50);
  grid.columns = columns;
  grid.grouping = grouping;

  // Grouping is a built-in feature, no plugin needed
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.hideAttribution = true;

  document.querySelector(parentSelector)?.appendChild(grid);
}
Vue vue
<template>
  <RevoGrid
    class="rounded-lg overflow-hidden cell-border"
    :columns="columns"
    :source="source"
    :grouping="grouping"
    :theme="isDark ? 'darkMaterial' : 'material'"
    hide-attribution
  />
</template>

<script setup lang="ts">
import RevoGrid from '@revolist/vue3-datagrid';
import { ref } from 'vue';
import { currentThemeVue } from '../composables/useRandomData';
import { createGroupingData, columns, grouping } from './groupingData';

const { isDark } = currentThemeVue();

// Generate realistic data using faker.js
const source = ref(createGroupingData(50));

// Grouping is a built-in feature, no plugin needed
</script>
React tsx
import React, { useMemo } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { currentTheme } from '../composables/useRandomData';
import { createGroupingData, columns, grouping } from './groupingData';

const { isDark } = currentTheme();

function Grouping() {
  // Generate realistic data using faker.js
  const source = useMemo(() => createGroupingData(50), []);

  return (
    <RevoGrid
      source={source}
      columns={columns}
      grouping={grouping}
      theme={isDark() ? 'darkCompact' : 'compact'}
      hideAttribution
    />
  );
}

export default Grouping;
Angular ts
import { Component, OnInit } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { currentTheme } from '../composables/useRandomData';
import { createGroupingData, columns, grouping } from './groupingData';

@Component({
  selector: 'grouping-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <revo-grid
      [source]="source"
      [columns]="columns"
      [grouping]="grouping"
      [theme]="theme"
      [hideAttribution]="true"
      style="min-height: 400px;"
    ></revo-grid>
  `
})
export class GroupingGridComponent implements OnInit {
  // Generate realistic data using faker.js
  source = createGroupingData(50);
  columns = columns;
  grouping = grouping;
  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';

  ngOnInit() {
    // Any additional initialization logic
  }
}
Grouping Data ts
import { faker } from '@faker-js/faker';
import type { ColumnRegular, DataType, GroupingOptions } from '@revolist/revogrid';
import { groupingAggregation } from '@revolist/revogrid-pro';

export interface GroupingDataItem {
  id: number;
  category: string;
  subcategory: string;
  product: string;
  price: number;
  quantity: number;
  orderDate: string;
  deliveryDate: string;
  customer: string;
  region: string;
  status: 'pending' | 'shipped' | 'delivered' | 'cancelled';
}


// Define aggregation functions for different columns
const aggregations = {
  category: (values: DataType[]) => {
    return `${values.length} categories`;
  },
  orderDate: (values: DataType[]) => {
    const dates = values.map(date => new Date(date.orderDate));
    const earliest = new Date(Math.min(...dates.map(d => d.getTime())));
    const latest = new Date(Math.max(...dates.map(d => d.getTime())));
    return `${earliest.toLocaleDateString()} - ${latest.toLocaleDateString()}`;
  },
  subcategory: (values: DataType[]) => {
    return `${values.length} subcategories`;
  },
};

const categories = [
  { name: 'Electronics', subcategories: ['Phones', 'Laptops', 'Tablets', 'Accessories'] },
  { name: 'Clothing', subcategories: ['Shirts', 'Pants', 'Dresses', 'Shoes'] },
  { name: 'Books', subcategories: ['Fiction', 'Non-Fiction', 'Educational', 'Children'] },
  { name: 'Home & Garden', subcategories: ['Furniture', 'Kitchen', 'Decor', 'Tools'] },
  { name: 'Sports', subcategories: ['Fitness', 'Outdoor', 'Team Sports', 'Water Sports'] },
];

const regions = ['North', 'South', 'East', 'West', 'Central'];
const statuses: GroupingDataItem['status'][] = ['pending', 'shipped', 'delivered', 'cancelled'];

function generateProductName(category: string, subcategory: string): string {
  const productTemplates: Record<string, Record<string, string[]>> = {
    'Electronics': {
      'Phones': ['iPhone', 'Samsung Galaxy', 'Google Pixel', 'OnePlus', 'Xiaomi'],
      'Laptops': ['MacBook Pro', 'Dell XPS', 'HP Pavilion', 'Lenovo ThinkPad', 'ASUS ZenBook'],
      'Tablets': ['iPad', 'Samsung Tab', 'Surface Pro', 'Fire Tablet', 'Huawei MatePad'],
      'Accessories': ['Wireless Headphones', 'Phone Case', 'Charging Cable', 'Power Bank', 'Screen Protector'],
    },
    'Clothing': {
      'Shirts': ['Cotton T-Shirt', 'Polo Shirt', 'Dress Shirt', 'Hoodie', 'Tank Top'],
      'Pants': ['Jeans', 'Chinos', 'Sweatpants', 'Dress Pants', 'Shorts'],
      'Dresses': ['Summer Dress', 'Cocktail Dress', 'Maxi Dress', 'Mini Dress', 'Wrap Dress'],
      'Shoes': ['Sneakers', 'Boots', 'Sandals', 'Heels', 'Loafers'],
    },
    'Books': {
      'Fiction': ['Mystery Novel', 'Romance Novel', 'Sci-Fi Book', 'Fantasy Novel', 'Thriller'],
      'Non-Fiction': ['Biography', 'Self-Help Book', 'History Book', 'Cookbook', 'Travel Guide'],
      'Educational': ['Textbook', 'Reference Book', 'Study Guide', 'Language Book', 'Technical Manual'],
      'Children': ['Picture Book', 'Chapter Book', 'Activity Book', 'Fairy Tale', 'Comic Book'],
    },
    'Home & Garden': {
      'Furniture': ['Dining Table', 'Sofa', 'Bookshelf', 'Coffee Table', 'Bed Frame'],
      'Kitchen': ['Blender', 'Coffee Maker', 'Dinnerware Set', 'Cookware Set', 'Kitchen Knife'],
      'Decor': ['Wall Art', 'Vase', 'Candle Set', 'Throw Pillow', 'Picture Frame'],
      'Tools': ['Drill Set', 'Toolbox', 'Garden Shovel', 'Screwdriver Set', 'Measuring Tape'],
    },
    'Sports': {
      'Fitness': ['Yoga Mat', 'Dumbbells', 'Resistance Bands', 'Jump Rope', 'Foam Roller'],
      'Outdoor': ['Camping Tent', 'Hiking Backpack', 'Sleeping Bag', 'Water Bottle', 'Flashlight'],
      'Team Sports': ['Basketball', 'Soccer Ball', 'Tennis Racket', 'Baseball Glove', 'Volleyball'],
      'Water Sports': ['Swimming Goggles', 'Pool Noodle', 'Beach Ball', 'Snorkel Set', 'Water Shoes'],
    },
  };

  const products = productTemplates[category]?.[subcategory] || ['Generic Product'];
  return faker.helpers.arrayElement(products);
}

function generatePrice(category: string, subcategory: string): number {
  const priceRanges: Record<string, Record<string, { min: number; max: number }>> = {
    'Electronics': {
      'Phones': { min: 200, max: 1200 },
      'Laptops': { min: 500, max: 3000 },
      'Tablets': { min: 150, max: 800 },
      'Accessories': { min: 10, max: 200 },
    },
    'Clothing': {
      'Shirts': { min: 15, max: 80 },
      'Pants': { min: 30, max: 150 },
      'Dresses': { min: 40, max: 200 },
      'Shoes': { min: 50, max: 300 },
    },
    'Books': {
      'Fiction': { min: 8, max: 25 },
      'Non-Fiction': { min: 10, max: 35 },
      'Educational': { min: 15, max: 100 },
      'Children': { min: 5, max: 20 },
    },
    'Home & Garden': {
      'Furniture': { min: 100, max: 2000 },
      'Kitchen': { min: 20, max: 300 },
      'Decor': { min: 10, max: 150 },
      'Tools': { min: 15, max: 200 },
    },
    'Sports': {
      'Fitness': { min: 10, max: 100 },
      'Outdoor': { min: 25, max: 500 },
      'Team Sports': { min: 15, max: 200 },
      'Water Sports': { min: 10, max: 150 },
    },
  };

  const range = priceRanges[category]?.[subcategory] || { min: 10, max: 100 };
  return faker.number.float({ min: range.min, max: range.max, fractionDigits: 2 });
}

export function createGroupingData(count: number = 50): GroupingDataItem[] {
  return Array.from({ length: count }, (_, index) => {
    const category = faker.helpers.arrayElement(categories);
    const subcategory = faker.helpers.arrayElement(category.subcategories);
    
    // Generate realistic product names based on category and subcategory
    const product = generateProductName(category.name, subcategory);
    
    // Generate realistic prices based on category
    const price = generatePrice(category.name, subcategory);
    
    // Generate order date within the last 6 months
    const orderDate = faker.date.between({ 
      from: new Date(Date.now() - 6 * 30 * 24 * 60 * 60 * 1000), 
      to: new Date() 
    });
    
    // Generate delivery date 1-14 days after order date
    const deliveryDate = faker.date.between({ 
      from: orderDate, 
      to: new Date(orderDate.getTime() + 14 * 24 * 60 * 60 * 1000) 
    });

    return {
      id: index + 1,
      category: category.name,
      subcategory,
      product,
      price,
      quantity: faker.number.int({ min: 1, max: 20 }),
      orderDate: orderDate.toISOString().split('T')[0],
      deliveryDate: deliveryDate.toISOString().split('T')[0],
      customer: faker.person.fullName(),
      region: faker.helpers.arrayElement(regions),
      status: faker.helpers.arrayElement(statuses),
    };
  });
}

// Configure grouping by category and subcategory
export const grouping: GroupingOptions = {
  props: ['orderDate', 'category', 'subcategory'],
  getGroupValue: (model, prop) => {
    if (prop === 'orderDate') {
      return model[prop]?.split('-')[0] ?? '';
    }
    return model[prop] ?? '';
  },
  groupLabelTemplate: (h, props) => {
    if (props.colType === 'rgCol') {
      return groupingAggregation(h, props, aggregations);
    }
  }
};
export const columns: ColumnRegular[] = [
  { name: '🆔 ID', prop: 'id', size: 60 },
  { name: '📂 Category', prop: 'category', size: 120 },
  { name: '📁 Subcategory', prop: 'subcategory', size: 120 },
  { name: '📦 Product', prop: 'product', size: 150 },
  { name: '💰 Price', prop: 'price', size: 100 },
  { name: '📊 Quantity', prop: 'quantity', size: 100 },
  { name: '📅 Order Date', prop: 'orderDate', size: 120 },
  { name: '🚚 Delivery Date', prop: 'deliveryDate', size: 120 },
  { name: '👤 Customer', prop: 'customer', size: 150 },
  { name: '🌍 Region', prop: 'region', size: 100 },
  { name: '📋 Status', prop: 'status', size: 100 },
];
import { groupingAggregation } from '@revolist/revogrid-pro';

Create custom aggregation functions for different columns. Important: Aggregation functions receive an array of complete data objects, not just the column values. You need to extract the specific property you want to aggregate:

const aggregations = {
price: (values: any[]) => {
const prices = values.map(item => item.price);
const sum = prices.reduce((acc, val) => acc + val, 0);
const avg = sum / prices.length;
return `Avg: $${avg.toFixed(2)}`;
},
quantity: (values: any[]) => {
const quantities = values.map(item => item.quantity);
const total = quantities.reduce((acc, val) => acc + val, 0);
return `Total: ${total}`;
},
product: (values: any[]) => {
return `${values.length} products`;
}
};

Use the groupingAggregation function to create a template that displays aggregations:

const groupTemplate = (h: any, props: any) => {
return groupingAggregation(h, props, aggregations);
};

Apply the group template to columns that will be used for grouping:

const columns = [
{ name: '🆔 ID', prop: 'id', size: 60 },
{ name: '📂 Category', prop: 'category', size: 120, template: groupTemplate },
{ name: '📁 Subcategory', prop: 'subcategory', size: 120, template: groupTemplate },
{ name: '📦 Product', prop: 'product', size: 150 },
{ name: '💰 Price', prop: 'price', size: 100 },
{ name: '📊 Quantity', prop: 'quantity', size: 100 },
{ name: '📅 Order Date', prop: 'orderDate', size: 120 },
{ name: '🚚 Delivery Date', prop: 'deliveryDate', size: 120 },
{ name: '👤 Customer', prop: 'customer', size: 150 },
{ name: '🌍 Region', prop: 'region', size: 100 },
{ name: '📋 Status', prop: 'status', size: 100 },
];

Define which columns should be used for grouping:

const grouping = {
props: ['orderDate', 'category', 'subcategory']
};
const grid = document.createElement('revo-grid');
grid.source = yourData;
grid.columns = columns;
grid.grouping = grouping;
// Grouping is a built-in feature, no plugin needed
// Average
price: (values: any[]) => {
const prices = values.map(item => item.price);
const avg = prices.reduce((acc, val) => acc + val, 0) / prices.length;
return `Avg: $${avg.toFixed(2)}`;
}
// Sum
quantity: (values: any[]) => {
const quantities = values.map(item => item.quantity);
const total = quantities.reduce((acc, val) => acc + val, 0);
return `Total: ${total}`;
}
// Min/Max
price: (values: any[]) => {
const prices = values.map(item => item.price);
const min = Math.min(...prices);
const max = Math.max(...prices);
return `Range: $${min} - $${max}`;
}
// Count
product: (values: any[]) => {
return `${values.length} products`;
}
// Concatenation
names: (values: any[]) => {
const names = values.map(item => item.name);
return names.join(', ');
}
// Unique count
categories: (values: any[]) => {
const categories = values.map(item => item.category);
const unique = new Set(categories);
return `${unique.size} unique categories`;
}
// Date range
orderDate: (values: any[]) => {
const dates = values.map(item => new Date(item.orderDate));
const sorted = dates.sort((a, b) => a.getTime() - b.getTime());
const earliest = sorted[0].toLocaleDateString();
const latest = sorted[sorted.length - 1].toLocaleDateString();
return `${earliest} - ${latest}`;
}

You can customize the appearance of group headers by modifying the template function:

const customGroupTemplate = (h: any, props: any) => {
const aggregationValue = getAggregationValue(props);
return h('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '4px 8px',
backgroundColor: '#f0f0f0',
borderRadius: '4px'
}
}, [
h('span', { style: { fontWeight: 'bold' } }, props.name),
h('span', { style: { fontSize: '12px', color: '#666' } }, `(${aggregationValue})`)
]);
};

You can apply different aggregation logic based on the column or group:

const conditionalAggregations = {
price: (values: any[]) => {
const prices = values.map(item => item.price);
const sum = prices.reduce((a, b) => a + b, 0);
const avg = sum / prices.length;
return `Avg: $${avg.toFixed(2)}`;
}
};
  1. Performance: Keep aggregation functions lightweight, especially with large datasets
  2. User Experience: Provide clear, meaningful aggregation labels
  3. Data Types: Ensure aggregation functions handle the correct data types
  4. Error Handling: Add validation for edge cases (empty arrays, null values)
  5. Accessibility: Use descriptive text for screen readers
  • Sales Reports: Group by region/category with revenue totals
  • Inventory Management: Group by department with stock quantities
  • Financial Data: Group by account type with balance summaries
  • Project Management: Group by team with task counts and completion rates