Grouping with Aggregation
Overview
Section titled “Overview”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.
Key Features
Section titled “Key Features”- 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
Basic Usage
Section titled “Basic Usage”Source code
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);}<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.jsconst source = ref(createGroupingData(50));
// Grouping is a built-in feature, no plugin needed</script>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;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="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 }}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 columnsconst 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 subcategoryexport 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 },];Implementation
Section titled “Implementation”1. Import Required Components
Section titled “1. Import Required Components”import { groupingAggregation } from '@revolist/revogrid-pro';2. Define Aggregation Functions
Section titled “2. Define Aggregation Functions”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`; }};3. Create Group Template
Section titled “3. Create Group Template”Use the groupingAggregation function to create a template that displays aggregations:
const groupTemplate = (h: any, props: any) => { return groupingAggregation(h, props, aggregations);};4. Configure Columns
Section titled “4. Configure Columns”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 },];5. Set Up Grouping Configuration
Section titled “5. Set Up Grouping Configuration”Define which columns should be used for grouping:
const grouping = { props: ['orderDate', 'category', 'subcategory']};6. Initialize the Grid
Section titled “6. Initialize the Grid”const grid = document.createElement('revo-grid');grid.source = yourData;grid.columns = columns;grid.grouping = grouping;// Grouping is a built-in feature, no plugin neededAggregation Function Examples
Section titled “Aggregation Function Examples”Numeric Aggregations
Section titled “Numeric Aggregations”// Averageprice: (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)}`;}
// Sumquantity: (values: any[]) => { const quantities = values.map(item => item.quantity); const total = quantities.reduce((acc, val) => acc + val, 0); return `Total: ${total}`;}
// Min/Maxprice: (values: any[]) => { const prices = values.map(item => item.price); const min = Math.min(...prices); const max = Math.max(...prices); return `Range: $${min} - $${max}`;}String Aggregations
Section titled “String Aggregations”// Countproduct: (values: any[]) => { return `${values.length} products`;}
// Concatenationnames: (values: any[]) => { const names = values.map(item => item.name); return names.join(', ');}
// Unique countcategories: (values: any[]) => { const categories = values.map(item => item.category); const unique = new Set(categories); return `${unique.size} unique categories`;}Date Aggregations
Section titled “Date Aggregations”// Date rangeorderDate: (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}`;}Advanced Configuration
Section titled “Advanced Configuration”Custom Group Header Styling
Section titled “Custom Group Header Styling”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})`) ]);};Conditional Aggregations
Section titled “Conditional Aggregations”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)}`; }};Best Practices
Section titled “Best Practices”- Performance: Keep aggregation functions lightweight, especially with large datasets
- User Experience: Provide clear, meaningful aggregation labels
- Data Types: Ensure aggregation functions handle the correct data types
- Error Handling: Add validation for edge cases (empty arrays, null values)
- Accessibility: Use descriptive text for screen readers
Common Use Cases
Section titled “Common Use Cases”- 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