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 / ProductBerlinLondonMadridParisGrand Total
AmountUnitsAmountUnitsAmountUnitsAmountUnitsAmountUnits
Hardware$89,600100$127,50034$68,20019$98,40028$383,700181
Hardware / Laptops$51,20018$127,50034$68,20019$98,40028$345,30099
Hardware / Monitors$38,40082$38,40082
Services$66,30036$28,70064$118,60047$213,600147
Services / Consulting$66,30036$66,30036
Services / Implementation$118,60047$118,60047
Services / Training$28,70064$28,70064
Software$94,200205$76,800192$45,100135$124,500355$340,600887
Software / Licenses$94,200205$94,200205
Software / Subscriptions$124,500355$124,500355
Software / Support$76,800192$45,100135$121,900327
Grand Total$183,800305$270,600262$142,000218$341,500430$937,9001215
<PivotGrid TItem="PivotSale"
           Data="@Sales"
           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

Enable ShowToolbar and FieldChooser to show the built-in toolbar command and modal field chooser. Use ToolbarOptions to hide optional built-in toolbar buttons.
Category / ProductBerlinLondonMadridParisGrand Total
AmountUnitsAmountUnitsAmountUnitsAmountUnitsAmountUnits
Hardware$51,20018$127,50034$178,70052
Hardware / Laptops$51,20018$127,50034$178,70052
Services$66,30036$28,70064$95,000100
Services / Consulting$66,30036$66,30036
Services / Training$28,70064$28,70064
Software$94,200205$124,500355$218,700560
Software / Licenses$94,200205$94,200205
Software / Subscriptions$124,500355$124,500355
Grand Total$145,400223$193,80070$28,70064$124,500355$492,400712
<PivotGrid TItem="PivotSale"
           Data="@Sales"
           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

Use ToolbarTemplate to replace the built-in toolbar and invoke PivotGrid commands through the toolbar context.
Region / CategoryBerlinLondonMadridParisGrand 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="@Sales"
           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 / ProductBerlinLondonMadridParisGrand Total
AmountUnitsAmountUnitsAmountUnitsAmountUnitsAmountUnits
Cloud$455,000561$516,300621$738,800818$591,900606$2,302,0002606
Cloud / Backup$154,800196$172,100207$189,400218$206,700229$723,000850
Cloud / Compute$136,000142$170,600164$187,900175$494,500481
Cloud / Database$164,200223$181,500234$198,800245$544,500702
Cloud / Storage$162,700180$180,000191$197,300202$540,000573
Hardware$122,400216$213,600278$198,000201$259,300261$793,300956
Hardware / Laptops$39,30029$56,60040$73,90051$169,800120
Hardware / Monitors$31,40045$48,70056$66,00067$83,30078$229,400246
1 - 8 of 26 items
<Field>
    <Switch @bind-Value="@virtualize">Virtualize rows</Switch>
</Field>

<PivotGrid TItem="PivotSale"
           Data="@Sales"
           ShowPager="@(!virtualize)"
           ShowPageSizes="@(!virtualize)"
           PageSize="8"
           PageSizes="@pageSizes"
           Virtualize="@virtualize"
           VirtualizeOptions="@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="@ReadRemotePivotData"
           Virtualize
           VirtualizeOptions="@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.

No value fields are declared.
<Field>
    <FieldLabel>Provider</FieldLabel>
    <FieldBody>
        <Select TValue="ProviderMode" @bind-Value="@providerMode">
            <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="@CurrentReadData"
           DataProvider="@CurrentDataProvider"
           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 handle CellClicked to inspect the source items behind a pivot value.
CategoryCentralWesternGrand 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="@Sales"
           CellClicked="@OnCellClicked"
           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 / CategoryBerlinLondonMadridParisGrand 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="@Sales"
           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

Set Aggregator when a value field needs domain-specific aggregation instead of one of the built-in aggregate functions.
CategoryCentralWesternGrand Total
RevenueMargin %RevenueMargin %RevenueMargin %
Hardware$51,20018.0%$127,50021.0%$178,70020.1%
Services$66,30036.0%$66,30036.0%
Software$94,20042.0%$94,20042.0%
Grand Total$145,40033.5%$193,80026.1%$339,20029.3%
<PivotGrid TItem="MarginSale"
           Data="@Sales"
           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="@WeightedMargin"
                            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

Use CellStyling, RowHeaderStyling, and ColumnHeaderStyling to apply conditional styling.
Category / ProductBerlinLondonMadridParisGrand Total
AmountUnitsAmountUnitsAmountUnitsAmountUnitsAmountUnits
Hardware$51,20018$127,50034$178,70052
Hardware / Laptops$51,20018$127,50034$178,70052
Services$66,30036$28,70064$95,000100
Services / Consulting$66,30036$66,30036
Services / Training$28,70064$28,70064
Software$94,200205$124,500355$218,700560
Software / Licenses$94,200205$94,200205
Software / Subscriptions$124,500355$124,500355
Grand Total$145,400223$193,80070$28,70064$124,500355$492,400712
<PivotGrid TItem="PivotSale"
           Data="@Sales"
           CellStyling="@OnCellStyling"
           RowHeaderStyling="@OnRowHeaderStyling"
           ColumnHeaderStyling="@OnColumnHeaderStyling"
           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.

On this page