Blazorise PivotGrid component
Build read-only Excel-like pivot tables for multi-dimensional data analysis.
The PivotGrid component groups source records into row and column dimensions, calculates aggregate values, and renders totals and subtotals in a table layout. It is designed for analysis and reporting scenarios where users inspect summarized data instead of editing source records.
To use the PivotGrid component, install the Blazorise.PivotGrid package first.
Installation
NuGet
Install extension from NuGet.dotnet add package Blazorise.PivotGrid
Imports
In your main _Imports.razor add:
@using Blazorise.PivotGrid
Fundamentals
Data Areas
PivotGrid uses declarative child components to define each data area:
PivotGridColumns: fields that create column groups.PivotGridRows: fields that create row groups.PivotGridAggregates: value fields that calculate totals, such as sum or count.PivotGridFields: optional field catalog used by the runtime field chooser.
Runtime Layout
Enable FieldChooser when users should rearrange the pivot layout at runtime. The field chooser lets users move available fields into rows, columns, values, and filters.
Data Loading
Bind in-memory data with Data, handle callback loading with ReadData, or provide a reusable implementation with DataProvider. A data provider can be implemented in a separate package when the data comes from a specific backend or protocol.
Examples
Basic table
Define columns, rows, and aggregates declaratively.| Category / Product | Berlin | London | Madrid | Paris | Grand Total | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| Amount | Units | Amount | Units | Amount | Units | Amount | Units | Amount | Units | |
| Hardware | $89,600 | 100 | $127,500 | 34 | $68,200 | 19 | $98,400 | 28 | $383,700 | 181 |
| Hardware / Laptops | $51,200 | 18 | $127,500 | 34 | $68,200 | 19 | $98,400 | 28 | $345,300 | 99 |
| Hardware / Monitors | $38,400 | 82 | $38,400 | 82 | ||||||
| Services | $66,300 | 36 | $28,700 | 64 | $118,600 | 47 | $213,600 | 147 | ||
| Services / Consulting | $66,300 | 36 | $66,300 | 36 | ||||||
| Services / Implementation | $118,600 | 47 | $118,600 | 47 | ||||||
| Services / Training | $28,700 | 64 | $28,700 | 64 | ||||||
| Software | $94,200 | 205 | $76,800 | 192 | $45,100 | 135 | $124,500 | 355 | $340,600 | 887 |
| Software / Licenses | $94,200 | 205 | $94,200 | 205 | ||||||
| Software / Subscriptions | $124,500 | 355 | $124,500 | 355 | ||||||
| Software / Support | $76,800 | 192 | $45,100 | 135 | $121,900 | 327 | ||||
| Grand Total | $183,800 | 305 | $270,600 | 262 | $142,000 | 218 | $341,500 | 430 | $937,900 | 1215 |
<PivotGrid TItem="PivotSale" Data="" ShowRowTotals ShowColumnTotals ShowRowSubtotals Striped Hoverable> <PivotGridColumns> <PivotGridColumn Field="@nameof( PivotSale.City )" Caption="City" /> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( PivotSale.Category )" Caption="Category" /> <PivotGridRow Field="@nameof( PivotSale.Product )" Caption="Product" /> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( PivotSale.Amount )" Caption="Amount" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )" /> <PivotGridAggregate Field="@nameof( PivotSale.Units )" Caption="Units" Aggregate="PivotGridAggregateFunction.Sum" /> </PivotGridAggregates> </PivotGrid>
@code { private readonly List<PivotSale> Sales = [ new() { City = "Berlin", Category = "Hardware", Product = "Laptops", Amount = 51200m, Units = 18 }, new() { City = "Berlin", Category = "Hardware", Product = "Monitors", Amount = 38400m, Units = 82 }, new() { City = "Berlin", Category = "Software", Product = "Licenses", Amount = 94200m, Units = 205 }, new() { City = "London", Category = "Hardware", Product = "Laptops", Amount = 127500m, Units = 34 }, new() { City = "London", Category = "Services", Product = "Consulting", Amount = 66300m, Units = 36 }, new() { City = "London", Category = "Software", Product = "Support", Amount = 76800m, Units = 192 }, new() { City = "Madrid", Category = "Hardware", Product = "Laptops", Amount = 68200m, Units = 19 }, new() { City = "Madrid", Category = "Services", Product = "Training", Amount = 28700m, Units = 64 }, new() { City = "Madrid", Category = "Software", Product = "Support", Amount = 45100m, Units = 135 }, new() { City = "Paris", Category = "Hardware", Product = "Laptops", Amount = 98400m, Units = 28 }, new() { City = "Paris", Category = "Services", Product = "Implementation", Amount = 118600m, Units = 47 }, new() { City = "Paris", Category = "Software", Product = "Subscriptions", Amount = 124500m, Units = 355 }, ]; public class PivotSale { public string City { get; set; } public string Category { get; set; } public string Product { get; set; } public decimal Amount { get; set; } public int Units { get; set; } } }
Field Chooser
EnableShowToolbar and FieldChooser to show the built-in toolbar command and modal field chooser. Use ToolbarOptions to hide optional built-in toolbar buttons.
| Category / Product | Berlin | London | Madrid | Paris | Grand Total | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| Amount | Units | Amount | Units | Amount | Units | Amount | Units | Amount | Units | |
| Hardware | $51,200 | 18 | $127,500 | 34 | $178,700 | 52 | ||||
| Hardware / Laptops | $51,200 | 18 | $127,500 | 34 | $178,700 | 52 | ||||
| Services | $66,300 | 36 | $28,700 | 64 | $95,000 | 100 | ||||
| Services / Consulting | $66,300 | 36 | $66,300 | 36 | ||||||
| Services / Training | $28,700 | 64 | $28,700 | 64 | ||||||
| Software | $94,200 | 205 | $124,500 | 355 | $218,700 | 560 | ||||
| Software / Licenses | $94,200 | 205 | $94,200 | 205 | ||||||
| Software / Subscriptions | $124,500 | 355 | $124,500 | 355 | ||||||
| Grand Total | $145,400 | 223 | $193,800 | 70 | $28,700 | 64 | $124,500 | 355 | $492,400 | 712 |
<PivotGrid TItem="PivotSale" Data="" ShowToolbar FieldChooser ShowRowTotals ShowColumnTotals ShowRowSubtotals Striped Hoverable> <PivotGridFields> <PivotGridField Field="@nameof( PivotSale.Region )" Caption="Region" /> <PivotGridField Field="@nameof( PivotSale.City )" Caption="City" /> <PivotGridField Field="@nameof( PivotSale.Category )" Caption="Category" /> <PivotGridField Field="@nameof( PivotSale.Product )" Caption="Product" /> <PivotGridField Field="@nameof( PivotSale.Amount )" Caption="Amount" /> <PivotGridField Field="@nameof( PivotSale.Units )" Caption="Units" /> </PivotGridFields> <PivotGridColumns> <PivotGridColumn Field="@nameof( PivotSale.City )" Caption="City" /> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( PivotSale.Category )" Caption="Category" /> <PivotGridRow Field="@nameof( PivotSale.Product )" Caption="Product" /> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( PivotSale.Amount )" Caption="Amount" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )" /> <PivotGridAggregate Field="@nameof( PivotSale.Units )" Caption="Units" Aggregate="PivotGridAggregateFunction.Sum" /> </PivotGridAggregates> </PivotGrid>
@code { private readonly List<PivotSale> Sales = [ new() { Region = "Central", City = "Berlin", Category = "Hardware", Product = "Laptops", Amount = 51200m, Units = 18 }, new() { Region = "Central", City = "Berlin", Category = "Software", Product = "Licenses", Amount = 94200m, Units = 205 }, new() { Region = "Western", City = "London", Category = "Hardware", Product = "Laptops", Amount = 127500m, Units = 34 }, new() { Region = "Western", City = "London", Category = "Services", Product = "Consulting", Amount = 66300m, Units = 36 }, new() { Region = "Central", City = "Madrid", Category = "Services", Product = "Training", Amount = 28700m, Units = 64 }, new() { Region = "Western", City = "Paris", Category = "Software", Product = "Subscriptions", Amount = 124500m, Units = 355 }, ]; public class PivotSale { public string Region { get; set; } public string City { get; set; } public string Category { get; set; } public string Product { get; set; } public decimal Amount { get; set; } public int Units { get; set; } } }
Custom Toolbar
UseToolbarTemplate to replace the built-in toolbar and invoke PivotGrid commands through the toolbar context.
| Region / Category | Berlin | London | Madrid | Paris | Grand Total |
|---|---|---|---|---|---|
| Central | $145,400 | $145,400 | |||
| Southern | $28,700 | $28,700 | |||
| Western | $193,800 | $124,500 | $318,300 | ||
| Grand Total | $145,400 | $193,800 | $28,700 | $124,500 | $492,400 |
<PivotGrid TItem="ToolbarSale" Data="" FieldChooser ExpandableRows InitiallyExpanded="false" ShowRowTotals ShowColumnTotals ShowRowSubtotals Striped Hoverable> <ToolbarTemplate Context="toolbar"> <Div Padding="Padding.Is3" Border="Border.Is1" Flex="Flex.AlignItems.Center" Gap="Gap.Is2"> @if ( toolbar.CanOpenFieldChooser ) { <Button Color="Color.Primary" Outline Clicked="@(() => toolbar.OpenFieldChooserCommand())"> <Icon Name="IconName.List" Margin="Margin.Is2.FromEnd" /> @toolbar.FieldsText </Button> } <Button Color="Color.Success" Outline Clicked="@(() => toolbar.RefreshCommand())"> <Icon Name="IconName.Reply" Margin="Margin.Is2.FromEnd" /> @toolbar.RefreshText </Button> @if ( toolbar.CanExpandCollapseGroups ) { <Buttons> <Button Color="Color.Secondary" Outline Clicked="@(() => toolbar.ExpandAllCommand())"> <Icon Name="IconName.ChevronDoubleDown" /> </Button> <Button Color="Color.Secondary" Outline Clicked="@(() => toolbar.CollapseAllCommand())"> <Icon Name="IconName.ChevronDoubleUp" /> </Button> </Buttons> } <Button Color="Color.Secondary" Outline Disabled="@(!toolbar.CanResetLayout)" Clicked="@(() => toolbar.ResetLayoutCommand())"> <Icon Name="IconName.Undo" Margin="Margin.Is2.FromEnd" /> @toolbar.ResetLayoutText </Button> </Div> </ToolbarTemplate> <ChildContent> <PivotGridFields> <PivotGridField Field="@nameof( ToolbarSale.Region )" Caption="Region" /> <PivotGridField Field="@nameof( ToolbarSale.City )" Caption="City" /> <PivotGridField Field="@nameof( ToolbarSale.Category )" Caption="Category" /> <PivotGridField Field="@nameof( ToolbarSale.Product )" Caption="Product" /> <PivotGridField Field="@nameof( ToolbarSale.Amount )" Caption="Amount" /> </PivotGridFields> <PivotGridColumns> <PivotGridColumn Field="@nameof( ToolbarSale.City )" Caption="City" /> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( ToolbarSale.Region )" Caption="Region" /> <PivotGridRow Field="@nameof( ToolbarSale.Category )" Caption="Category" /> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( ToolbarSale.Amount )" Caption="Amount" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )" /> </PivotGridAggregates> </ChildContent> </PivotGrid>
@code { private readonly List<ToolbarSale> Sales = [ new() { Region = "Central", City = "Berlin", Category = "Hardware", Product = "Laptops", Amount = 51200m }, new() { Region = "Central", City = "Berlin", Category = "Software", Product = "Licenses", Amount = 94200m }, new() { Region = "Western", City = "London", Category = "Hardware", Product = "Laptops", Amount = 127500m }, new() { Region = "Western", City = "London", Category = "Services", Product = "Consulting", Amount = 66300m }, new() { Region = "Southern", City = "Madrid", Category = "Services", Product = "Training", Amount = 28700m }, new() { Region = "Western", City = "Paris", Category = "Software", Product = "Subscriptions", Amount = 124500m }, ]; public class ToolbarSale { public string Region { get; set; } public string City { get; set; } public string Category { get; set; } public string Product { get; set; } public decimal Amount { get; set; } } }
Paging
Use ShowPager for explicit page navigation, or Virtualize when users should scroll through a larger pivot result.
Virtualize only limits rendered rows. When using local Data, the full pivot result is still computed before rows are virtualized. For large remote datasets, use ReadData or DataProvider with PivotGridReadDataMode.Virtualize and return prepared PivotGridResult rows for the requested range.
The pager range reports pivot result rows. When PageByGroups is enabled, paging is based on top-level row groups instead of every expanded detail row.
| Category / Product | Berlin | London | Madrid | Paris | Grand Total | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| Amount | Units | Amount | Units | Amount | Units | Amount | Units | Amount | Units | |
| Cloud | $455,000 | 561 | $516,300 | 621 | $738,800 | 818 | $591,900 | 606 | $2,302,000 | 2606 |
| Cloud / Backup | $154,800 | 196 | $172,100 | 207 | $189,400 | 218 | $206,700 | 229 | $723,000 | 850 |
| Cloud / Compute | $136,000 | 142 | $170,600 | 164 | $187,900 | 175 | $494,500 | 481 | ||
| Cloud / Database | $164,200 | 223 | $181,500 | 234 | $198,800 | 245 | $544,500 | 702 | ||
| Cloud / Storage | $162,700 | 180 | $180,000 | 191 | $197,300 | 202 | $540,000 | 573 | ||
| Hardware | $122,400 | 216 | $213,600 | 278 | $198,000 | 201 | $259,300 | 261 | $793,300 | 956 |
| Hardware / Laptops | $39,300 | 29 | $56,600 | 40 | $73,900 | 51 | $169,800 | 120 | ||
| Hardware / Monitors | $31,400 | 45 | $48,700 | 56 | $66,000 | 67 | $83,300 | 78 | $229,400 | 246 |
<Field> <Switch @bind-Value="">Virtualize rows</Switch> </Field> <PivotGrid TItem="PivotSale" Data="" ShowPager="@(!virtualize)" ShowPageSizes="@(!virtualize)" PageSize="8" PageSizes="" Virtualize="" VirtualizeOptions="" ShowRowTotals ShowColumnTotals ShowRowSubtotals Striped Hoverable Narrow> <PivotGridColumns> <PivotGridColumn Field="@nameof( PivotSale.City )" Caption="City" /> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( PivotSale.Category )" Caption="Category" /> <PivotGridRow Field="@nameof( PivotSale.Product )" Caption="Product" /> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( PivotSale.Amount )" Caption="Amount" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )" /> <PivotGridAggregate Field="@nameof( PivotSale.Units )" Caption="Units" Aggregate="PivotGridAggregateFunction.Sum" /> </PivotGridAggregates> </PivotGrid>
@code { private bool virtualize; private readonly int[] pageSizes = [8, 16, 32]; private readonly PivotGridVirtualizeOptions virtualizeOptions = new() { Height = "420px", MaxHeight = "420px", ItemSize = 44f, OverscanCount = 6, }; private readonly List<PivotSale> Sales = CreateSales(); private static List<PivotSale> CreateSales() { string[] cities = ["Berlin", "London", "Madrid", "Paris"]; (string Category, string[] Products)[] rows = [ ("Hardware", ["Laptops", "Monitors", "Tablets", "Peripherals"]), ("Software", ["Licenses", "Subscriptions", "Support", "Analytics"]), ("Services", ["Consulting", "Implementation", "Training", "Migration"]), ("Security", ["Firewall", "Identity", "Monitoring", "Audit"]), ("Cloud", ["Compute", "Storage", "Backup", "Database"]), ]; List<PivotSale> data = new(); for ( int rowIndex = 0; rowIndex < rows.Length; rowIndex++ ) { for ( int productIndex = 0; productIndex < rows[rowIndex].Products.Length; productIndex++ ) { for ( int cityIndex = 0; cityIndex < cities.Length; cityIndex++ ) { if ( ( rowIndex + productIndex + cityIndex ) % 5 == 0 ) continue; data.Add( new() { City = cities[cityIndex], Category = rows[rowIndex].Category, Product = rows[rowIndex].Products[productIndex], Amount = 22000m + ( rowIndex * 28500m ) + ( productIndex * 9400m ) + ( cityIndex * 17300m ), Units = 18 + ( rowIndex * 31 ) + ( productIndex * 27 ) + ( cityIndex * 11 ), } ); } } } return data; } public class PivotSale { public string City { get; set; } public string Category { get; set; } public string Product { get; set; } public decimal Amount { get; set; } public int Units { get; set; } } }
Remote Virtualization
For remote virtualization, return the requested pivot result rows through Result and set TotalItems to the total pivot result row count.
When row or column expansion is enabled, use IsRowGroupExpanded and IsColumnGroupExpanded from the request to build the requested expanded result window.
When returning a prepared PivotGridResult, keep the result shape consistent. DataColumns defines the cell order, so every returned PivotGridResultRow should contain the same number of Cells, in the same order. Data columns and aggregate metadata should stay stable across virtualized requests. Row fields, column fields, aggregate info, data columns, rows, and cells should not be null.
| Customer |
|---|
<PivotGrid TItem="RemotePivotSale" ReadData="" Virtualize VirtualizeOptions="" Striped Hoverable> <PivotGridColumns> <PivotGridColumn Field="@nameof( RemotePivotSale.Quarter )" Caption="Quarter" /> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( RemotePivotSale.Customer )" Caption="Customer" /> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( RemotePivotSale.Amount )" Caption="Amount" Aggregate="PivotGridAggregateFunction.Sum" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )" /> </PivotGridAggregates> </PivotGrid>
@code { private static readonly string[] quarters = ["Q1", "Q2", "Q3", "Q4"]; private static readonly IReadOnlyList<RemotePivotSale> sales = CreateSales(); private static readonly int totalPivotRows = sales .Select( sale => sale.Customer ) .Distinct() .Count(); private readonly PivotGridVirtualizeOptions virtualizeOptions = new() { Height = "420px", MaxHeight = "420px", ItemSize = 44f, OverscanCount = 6, }; private Task ReadRemotePivotData( PivotGridReadDataEventArgs<RemotePivotSale> eventArgs ) { PivotGridDataRequest request = eventArgs.Request; int offset = request.ReadDataMode == PivotGridReadDataMode.Virtualize ? Math.Max( 0, request.VirtualizeOffset ) : 0; int count = request.ReadDataMode == PivotGridReadDataMode.Virtualize && request.VirtualizeCount > 0 ? request.VirtualizeCount : 40; eventArgs.Result = CreatePivotResult( offset, count ); eventArgs.TotalItems = totalPivotRows; eventArgs.IsPaged = true; return Task.CompletedTask; } private static PivotGridResult<RemotePivotSale> CreatePivotResult( int offset, int count ) { IReadOnlyList<PivotGridFieldInfo<RemotePivotSale>> rowFields = [ new() { Field = nameof( RemotePivotSale.Customer ), Caption = "Customer", }, ]; IReadOnlyList<PivotGridFieldInfo<RemotePivotSale>> columnFields = [ new() { Field = nameof( RemotePivotSale.Quarter ), Caption = "Quarter", }, ]; PivotGridAggregateInfo<RemotePivotSale> aggregate = new() { Field = nameof( RemotePivotSale.Amount ), Caption = "Amount", Aggregate = PivotGridAggregateFunction.Sum, DisplayFormat = "{0:C0}", DisplayFormatProvider = System.Globalization.CultureInfo.GetCultureInfo( "en-US" ), }; IReadOnlyList<PivotGridAggregateInfo<RemotePivotSale>> aggregates = [aggregate]; List<PivotGridAxisItem<RemotePivotSale>> columnItems = quarters .Select( quarter => new PivotGridAxisItem<RemotePivotSale>( [quarter], sales.Where( sale => sale.Quarter == quarter ).ToList(), 0, false, false ) ) .ToList(); List<PivotGridDataColumn<RemotePivotSale>> dataColumns = columnItems .Select( column => new PivotGridDataColumn<RemotePivotSale>( column, aggregate ) ) .ToList(); List<PivotGridResultRow<RemotePivotSale>> rows = sales .GroupBy( sale => sale.Customer ) .OrderBy( group => group.Key ) .Skip( offset ) .Take( count ) .Select( group => { List<RemotePivotSale> rowItems = group.ToList(); PivotGridAxisItem<RemotePivotSale> row = new( [group.Key], rowItems, 0, false, false ); List<PivotGridCell<RemotePivotSale>> cells = dataColumns .Select( dataColumn => { string quarter = dataColumn.Column.Values[0]?.ToString(); List<RemotePivotSale> cellItems = rowItems .Where( sale => sale.Quarter == quarter ) .ToList(); object value = aggregate.Calculate( cellItems ); return new PivotGridCell<RemotePivotSale>( dataColumn, value, aggregate.FormatValue( value ), cellItems, false, false, false ); } ) .ToList(); return new PivotGridResultRow<RemotePivotSale>( row, cells ); } ) .ToList(); return new( rowFields, columnFields, aggregates, dataColumns, rows ); } private static List<RemotePivotSale> CreateSales() { List<RemotePivotSale> data = new(); for ( int customerIndex = 1; customerIndex <= 500; customerIndex++ ) { for ( int quarterIndex = 0; quarterIndex < quarters.Length; quarterIndex++ ) { data.Add( new() { Customer = $"Customer {customerIndex:000}", Quarter = quarters[quarterIndex], Amount = 8500m + ( customerIndex * 157m ) + ( quarterIndex * 3200m ), } ); } } return data; } public class RemotePivotSale { public string Customer { get; set; } public string Quarter { get; set; } public decimal Amount { get; set; } } }
Data loading
Use ReadData for callback-based loading, or DataProvider when the provider should be reusable. For server-side paging, return a prepared PivotGridResult, set IsPaged, and report the total pivot result row count with TotalItems.
When the field chooser is enabled with remote or prepared results, populate FilterOptions with all available filter values keyed by field name. Selected option keys are sent back on the corresponding request filter as FilterValueKey. Otherwise the field chooser can only infer filter values from the current raw Data page.
Prepared results returned by providers are normalized before rendering. Null structural entries are ignored, row cells are aligned to DataColumns, and missing cells are rendered as empty cells so headers, expansion, and cells stay consistent.
<Field> <FieldLabel>Provider</FieldLabel> <FieldBody> <Select TValue="ProviderMode" @bind-Value=""> <SelectItem TValue="ProviderMode" Value="ProviderMode.ReadData">ReadData callback</SelectItem> <SelectItem TValue="ProviderMode" Value="ProviderMode.DataProvider">DataProvider implementation</SelectItem> </Select> </FieldBody> </Field> <PivotGrid TItem="RemoteSale" ReadData="" DataProvider="" ShowPager PageSize="4" ShowRowTotals Striped Hoverable> <PivotGridColumns> <PivotGridColumn Field="@nameof( RemoteSale.Quarter )" Caption="Quarter" /> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( RemoteSale.Customer )" Caption="Customer" /> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( RemoteSale.Amount )" Caption="Amount" Aggregate="PivotGridAggregateFunction.Sum" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )" /> </PivotGridAggregates> </PivotGrid>
@code { private static readonly string[] quarters = ["Q1", "Q2", "Q3", "Q4"]; private static readonly IReadOnlyList<RemoteSale> sales = CreateSales(); private readonly IPivotGridDataProvider<RemoteSale> dataProvider = new RemoteSaleDataProvider( sales ); private ProviderMode providerMode = ProviderMode.ReadData; private EventCallback<PivotGridReadDataEventArgs<RemoteSale>> CurrentReadData => providerMode == ProviderMode.ReadData ? EventCallback.Factory.Create<PivotGridReadDataEventArgs<RemoteSale>>( this, ReadRemoteData ) : default; private IPivotGridDataProvider<RemoteSale> CurrentDataProvider => providerMode == ProviderMode.DataProvider ? dataProvider : null; private Task ReadRemoteData( PivotGridReadDataEventArgs<RemoteSale> eventArgs ) { PivotGridDataResult<RemoteSale> result = CreatePagedPivotResult( sales, eventArgs.Request ); eventArgs.Result = result.Result; eventArgs.TotalItems = result.TotalItems; eventArgs.IsPaged = result.IsPaged; return Task.CompletedTask; } private static PivotGridDataResult<RemoteSale> CreatePagedPivotResult( IReadOnlyList<RemoteSale> source, PivotGridDataRequest request ) { PivotGridFieldInfo<RemoteSale> rowField = new() { Field = nameof( RemoteSale.Customer ), Caption = "Customer", }; PivotGridFieldInfo<RemoteSale> columnField = new() { Field = nameof( RemoteSale.Quarter ), Caption = "Quarter", }; PivotGridAggregateInfo<RemoteSale> aggregate = new() { Field = nameof( RemoteSale.Amount ), Caption = "Amount", Aggregate = PivotGridAggregateFunction.Sum, DisplayFormat = "{0:C0}", DisplayFormatProvider = System.Globalization.CultureInfo.GetCultureInfo( "en-US" ), }; List<PivotGridAxisItem<RemoteSale>> columnItems = quarters .Select( quarter => new PivotGridAxisItem<RemoteSale>( [quarter], source.Where( sale => sale.Quarter == quarter ).ToList(), 0, false, false ) ) .ToList(); if ( request.ShowRowTotals ) { columnItems.Add( new PivotGridAxisItem<RemoteSale>( [], source, 0, false, true ) ); } List<PivotGridDataColumn<RemoteSale>> dataColumns = columnItems .Select( column => new PivotGridDataColumn<RemoteSale>( column, aggregate ) ) .ToList(); List<IGrouping<string, RemoteSale>> customerGroups = source .GroupBy( sale => sale.Customer ) .OrderBy( group => group.Key ) .ToList(); int totalRows = customerGroups.Count; int skip = request.ReadDataMode == PivotGridReadDataMode.Paging ? Math.Max( 0, ( request.Page - 1 ) * request.PageSize ) : 0; int take = request.ReadDataMode == PivotGridReadDataMode.Paging ? request.PageSize : totalRows; List<PivotGridResultRow<RemoteSale>> rows = customerGroups .Skip( skip ) .Take( take ) .Select( group => { List<RemoteSale> rowItems = group.ToList(); PivotGridAxisItem<RemoteSale> row = new( [group.Key], rowItems, 0, false, false ); List<PivotGridCell<RemoteSale>> cells = dataColumns .Select( dataColumn => { List<RemoteSale> cellItems = dataColumn.Column.IsGrandTotal ? rowItems : rowItems.Where( sale => sale.Quarter == dataColumn.Column.Values[0]?.ToString() ).ToList(); object value = aggregate.Calculate( cellItems ); return new PivotGridCell<RemoteSale>( dataColumn, value, aggregate.FormatValue( value ), cellItems, dataColumn.Column.IsTotal || dataColumn.Column.IsGrandTotal, false, false ); } ) .ToList(); return new PivotGridResultRow<RemoteSale>( row, cells ); } ) .ToList(); return new() { Result = new( [rowField], [columnField], [aggregate], dataColumns, rows ), TotalItems = totalRows, IsPaged = true, }; } private static List<RemoteSale> CreateSales() { List<RemoteSale> data = new(); for ( int customerIndex = 1; customerIndex <= 18; customerIndex++ ) { foreach ( string quarter in quarters ) { data.Add( new() { Customer = $"Customer {customerIndex:00}", Quarter = quarter, Amount = 12000m + ( customerIndex * 950m ) + ( Array.IndexOf( quarters, quarter ) * 2250m ), } ); } } return data; } private enum ProviderMode { ReadData, DataProvider, } private sealed class RemoteSaleDataProvider : IPivotGridDataProvider<RemoteSale> { private readonly IReadOnlyList<RemoteSale> source; public RemoteSaleDataProvider( IReadOnlyList<RemoteSale> source ) { this.source = source; } public Task<PivotGridDataResult<RemoteSale>> ReadDataAsync( PivotGridDataRequest request, System.Threading.CancellationToken cancellationToken ) => Task.FromResult( CreatePagedPivotResult( source, request ) ); } public class RemoteSale { public string Customer { get; set; } public string Quarter { get; set; } public decimal Amount { get; set; } } }
Cell templates
Customize headers, row and column values, aggregate cells, and handleCellClicked to inspect the source items behind a pivot value.
| Category | Central | Western | Grand Total |
|---|---|---|---|
| Hardware | $51,200 | $127,500 | $178,700 |
| Services | $66,300 | $66,300 | |
| Software | $94,200 | $94,200 | |
| Grand Total | $145,400 | $193,800 | $339,200 |
<PivotGrid TItem="TemplateSale" Data="" CellClicked="" ShowRowTotals ShowColumnTotals Striped Hoverable> <PivotGridColumns> <PivotGridColumn Field="@nameof( TemplateSale.Region )" Caption="Region"> <DisplayTemplate Context="context"> <Span TextWeight="TextWeight.SemiBold" TextTransform="TextTransform.Uppercase">@context.FormattedValue</Span> </DisplayTemplate> </PivotGridColumn> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( TemplateSale.Category )" Caption="Category"> <HeaderTemplate Context="context"> <Span TextWeight="TextWeight.Bold">@context.Caption</Span> </HeaderTemplate> <DisplayTemplate Context="context"> <Span TextColor="@(context.IsTotal ? TextColor.Primary : TextColor.Body)">@context.FormattedValue</Span> </DisplayTemplate> </PivotGridRow> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( TemplateSale.Amount )" Caption="Revenue" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )"> <CellTemplate Context="context"> <Span TextWeight="@(context.IsGrandTotal ? TextWeight.Bold : TextWeight.Default)"> @context.FormattedValue </Span> </CellTemplate> </PivotGridAggregate> </PivotGridAggregates> </PivotGrid> <Alert Color="Color.Info" Visible="@(!string.IsNullOrWhiteSpace( selectedCellText ))" Margin="Margin.Is3.FromTop"> @selectedCellText </Alert>
@code { private string selectedCellText; private readonly List<TemplateSale> Sales = [ new() { Region = "Central", Category = "Hardware", Amount = 51200m }, new() { Region = "Central", Category = "Software", Amount = 94200m }, new() { Region = "Western", Category = "Hardware", Amount = 127500m }, new() { Region = "Western", Category = "Services", Amount = 66300m }, ]; private Task OnCellClicked( PivotGridCellClickedEventArgs<TemplateSale> eventArgs ) { selectedCellText = $"{eventArgs.Aggregate.Caption}: {eventArgs.Value:C0} from {eventArgs.Items.Count} source rows"; return Task.CompletedTask; } public class TemplateSale { public string Region { get; set; } public string Category { get; set; } public decimal Amount { get; set; } } }
Filtering
Use the field chooser to move fields into filters, and enable expandable rows or columns when users should drill into grouped results.
Expandable grouped rows and columns render section headers independently from subtotal display, so users can still expand or collapse groups when subtotals are hidden.
| Region / Category | Berlin | London | Madrid | Paris | Grand Total |
|---|---|---|---|---|---|
| Central | $145,400 | $145,400 | |||
| Southern | $73,800 | $73,800 | |||
| Western | $193,800 | $243,100 | $436,900 | ||
| Grand Total | $145,400 | $193,800 | $73,800 | $243,100 | $656,100 |
<PivotGrid TItem="FilterSale" Data="" ShowToolbar FieldChooser ExpandableRows ExpandableColumns InitiallyExpanded="false" ShowRowTotals ShowColumnTotals ShowRowSubtotals ShowColumnSubtotals Striped Hoverable> <PivotGridFields> <PivotGridField Field="@nameof( FilterSale.Region )" Caption="Region" /> <PivotGridField Field="@nameof( FilterSale.City )" Caption="City" /> <PivotGridField Field="@nameof( FilterSale.Category )" Caption="Category" /> <PivotGridField Field="@nameof( FilterSale.Amount )" Caption="Amount" /> </PivotGridFields> <PivotGridColumns> <PivotGridColumn Field="@nameof( FilterSale.City )" Caption="City" /> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( FilterSale.Region )" Caption="Region" /> <PivotGridRow Field="@nameof( FilterSale.Category )" Caption="Category" /> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( FilterSale.Amount )" Caption="Amount" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )" /> </PivotGridAggregates> </PivotGrid>
@code { private readonly List<FilterSale> Sales = [ new() { Region = "Central", City = "Berlin", Category = "Hardware", Amount = 51200m }, new() { Region = "Central", City = "Berlin", Category = "Software", Amount = 94200m }, new() { Region = "Western", City = "London", Category = "Hardware", Amount = 127500m }, new() { Region = "Western", City = "London", Category = "Services", Amount = 66300m }, new() { Region = "Southern", City = "Madrid", Category = "Services", Amount = 28700m }, new() { Region = "Southern", City = "Madrid", Category = "Software", Amount = 45100m }, new() { Region = "Western", City = "Paris", Category = "Software", Amount = 124500m }, new() { Region = "Western", City = "Paris", Category = "Services", Amount = 118600m }, ]; public class FilterSale { public string Region { get; set; } public string City { get; set; } public string Category { get; set; } public decimal Amount { get; set; } } }
Custom Aggregator
SetAggregator when a value field needs domain-specific aggregation instead of one of the built-in aggregate functions.
| Category | Central | Western | Grand Total | |||
|---|---|---|---|---|---|---|
| Revenue | Margin % | Revenue | Margin % | Revenue | Margin % | |
| Hardware | $51,200 | 18.0% | $127,500 | 21.0% | $178,700 | 20.1% |
| Services | $66,300 | 36.0% | $66,300 | 36.0% | ||
| Software | $94,200 | 42.0% | $94,200 | 42.0% | ||
| Grand Total | $145,400 | 33.5% | $193,800 | 26.1% | $339,200 | 29.3% |
<PivotGrid TItem="MarginSale" Data="" ShowRowTotals ShowColumnTotals Striped Hoverable> <PivotGridColumns> <PivotGridColumn Field="@nameof( MarginSale.Region )" Caption="Region" /> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( MarginSale.Category )" Caption="Category" /> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( MarginSale.Amount )" Caption="Revenue" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )" /> <PivotGridAggregate TItem="MarginSale" Field="@nameof( MarginSale.Margin )" Caption="Margin %" Aggregator="" DisplayFormat="{0:P1}" /> </PivotGridAggregates> </PivotGrid>
@code { private readonly List<MarginSale> Sales = [ new() { Region = "Central", Category = "Hardware", Amount = 51200m, Margin = .18m }, new() { Region = "Central", Category = "Software", Amount = 94200m, Margin = .42m }, new() { Region = "Western", Category = "Hardware", Amount = 127500m, Margin = .21m }, new() { Region = "Western", Category = "Services", Amount = 66300m, Margin = .36m }, ]; private static object WeightedMargin( IReadOnlyList<MarginSale> items ) { decimal totalAmount = items.Sum( item => item.Amount ); if ( totalAmount == 0m ) return null; return items.Sum( item => item.Amount * item.Margin ) / totalAmount; } public class MarginSale { public string Region { get; set; } public string Category { get; set; } public decimal Amount { get; set; } public decimal Margin { get; set; } } }
Styling
UseCellStyling, RowHeaderStyling, and ColumnHeaderStyling to apply conditional styling.
| Category / Product | Berlin | London | Madrid | Paris | Grand Total | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| Amount | Units | Amount | Units | Amount | Units | Amount | Units | Amount | Units | |
| Hardware | $51,200 | 18 | $127,500 | 34 | $178,700 | 52 | ||||
| Hardware / Laptops | $51,200 | 18 | $127,500 | 34 | $178,700 | 52 | ||||
| Services | $66,300 | 36 | $28,700 | 64 | $95,000 | 100 | ||||
| Services / Consulting | $66,300 | 36 | $66,300 | 36 | ||||||
| Services / Training | $28,700 | 64 | $28,700 | 64 | ||||||
| Software | $94,200 | 205 | $124,500 | 355 | $218,700 | 560 | ||||
| Software / Licenses | $94,200 | 205 | $94,200 | 205 | ||||||
| Software / Subscriptions | $124,500 | 355 | $124,500 | 355 | ||||||
| Grand Total | $145,400 | 223 | $193,800 | 70 | $28,700 | 64 | $124,500 | 355 | $492,400 | 712 |
<PivotGrid TItem="PivotSale" Data="" CellStyling="" RowHeaderStyling="" ColumnHeaderStyling="" ShowRowTotals ShowColumnTotals ShowRowSubtotals Striped Hoverable> <PivotGridColumns> <PivotGridColumn Field="@nameof( PivotSale.City )" Caption="City" /> </PivotGridColumns> <PivotGridRows> <PivotGridRow Field="@nameof( PivotSale.Category )" Caption="Category" /> <PivotGridRow Field="@nameof( PivotSale.Product )" Caption="Product" /> </PivotGridRows> <PivotGridAggregates> <PivotGridAggregate Field="@nameof( PivotSale.Amount )" Caption="Amount" DisplayFormat="{0:C0}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo( "en-US" )" /> <PivotGridAggregate Field="@nameof( PivotSale.Units )" Caption="Units" Aggregate="PivotGridAggregateFunction.Sum" /> </PivotGridAggregates> </PivotGrid>
@code { private readonly List<PivotSale> Sales = [ new() { City = "Berlin", Category = "Hardware", Product = "Laptops", Amount = 51200m, Units = 18 }, new() { City = "Berlin", Category = "Software", Product = "Licenses", Amount = 94200m, Units = 205 }, new() { City = "London", Category = "Hardware", Product = "Laptops", Amount = 127500m, Units = 34 }, new() { City = "London", Category = "Services", Product = "Consulting", Amount = 66300m, Units = 36 }, new() { City = "Madrid", Category = "Services", Product = "Training", Amount = 28700m, Units = 64 }, new() { City = "Paris", Category = "Software", Product = "Subscriptions", Amount = 124500m, Units = 355 }, ]; private void OnCellStyling( PivotGridCellContext<PivotSale> context, PivotGridCellStyling styling ) { if ( context.Aggregate.Field != nameof( PivotSale.Amount ) || context.Value is not decimal amount ) return; if ( amount >= 100000m ) { styling.Background = Background.Success; styling.TextColor = TextColor.White; styling.TextWeight = TextWeight.Bold; } else if ( amount <= 30000m ) { styling.Background = Background.Danger; styling.TextColor = TextColor.White; styling.TextWeight = TextWeight.Bold; } } private void OnRowHeaderStyling( PivotGridRowHeaderContext<PivotSale> context, PivotGridCellStyling styling ) { if ( context.IsGrandTotal ) { styling.Background = Background.Info; styling.TextColor = TextColor.White; styling.TextWeight = TextWeight.Bold; } else if ( context.IsTotal ) { styling.Background = Background.Secondary; styling.TextColor = TextColor.White; styling.TextWeight = TextWeight.Bold; } } private void OnColumnHeaderStyling( PivotGridColumnHeaderContext<PivotSale> context, PivotGridCellStyling styling ) { if ( context.IsGrandTotal || context.IsTotal ) { styling.Background = Background.Info; styling.TextColor = TextColor.White; styling.TextWeight = TextWeight.Bold; } } public class PivotSale { public string City { get; set; } public string Category { get; set; } public string Product { get; set; } public decimal Amount { get; set; } public int Units { get; set; } } }
API
See the API reference for the parameters, events, methods, and related types available to the components covered on this page.