nextgenSOFTWARE
SOFTWARE NEXT GENERATION

NgPivotGrid — Pivot grid

Live component previewTry the controls · demonstration data
Open preview ↗

Manual · Getting started · Code examples · Models

When to use it

Aggregate records by row and column dimensions.

Appearance and live example

Try the Blazor example · Flutter example code

The live Blazor preview is embedded above. Use Reset to restore its initial state. Flutter implementation code is provided below; run the Flutter gallery from the installed developer workspace. Dialogs and drawers start closed; use their Open button. Tooltips appear on hover or keyboard focus.

Behavior and practical rules

NgPivotRecord contains a unique nonblank ID, string dimensions and one nullable finite numeric value. RowField/ColumnField must be nonblank. Raw dimension keys sort lexically; a missing key is the blank string. Sum/Average/Min/Max use numeric values; Count includes records with null values. Totals always aggregate original records, so an average is not an average of averages.

ShowRowTotals/showRowTotals controls the last column of per-row totals. ShowColumnTotals/showColumnTotals controls the bottom row of per-column totals. Both default true; the grand total is visible only when both are true. Visibility changes presentation, not aggregation. ValueFormatter/valueFormatter receives the nullable aggregate and returns plain text for visible and accessible values; null means no numeric result or overflow. RowLabel/rowLabel and ColumnLabel/columnLabel format raw keys, including the blank string, without changing sorting or callback keys. TotalText/totalText labels totals independently of ordinary keys; EmptyText/emptyText replaces the no-data message. These are plain-text formatters, not arbitrary widget/HTML templates.

SelectionReadOnly/selectionReadOnly blocks cell and total actions while keeping values readable. Missing callbacks also disable these actions. Input updates still work while locked. Stale callbacks after parent updates or disposal cannot emit old data. Cell callbacks contain original records in a read-only collection; null Row/Column denotes a total, while an empty string denotes a blank dimension. Replace input lists and dimension dictionaries instead of mutating them. This component emits drilldown requests and does not own current selection; the example retains Last drilldown as application history after data changes. Filter Items/items before passing them in. Multiple grouping levels, measures, OLAP, export, virtualization and a drag/drop configurator remain unimplemented.

Blazor example

Razor / Blazor
<div class="toolbar">
<NgButton OnClick="()=>rowTotals=!rowTotals">Toggle row totals</NgButton>
<NgButton OnClick="()=>columnTotals=!columnTotals">Toggle column totals</NgButton>
<NgButton OnClick="()=>locked=!locked">Toggle selection lock</NgButton>
<NgButton OnClick="()=>average=!average">Toggle average</NgButton>
<NgButton OnClick="()=>empty=!empty">Toggle empty data</NgButton>
<NgButton OnClick="()=>records=Data(2)">Update values</NgButton>
</div>
<p>Row totals @(rowTotals?"shown":"hidden") · Column totals @(columnTotals?"shown":"hidden") · Selection @(locked?"locked":"enabled")</p>
<NgPivotGrid Items="@(empty?[]:records)" Aggregate="@(average?NgAggregate.Average:NgAggregate.Sum)" ShowRowTotals="rowTotals" ShowColumnTotals="columnTotals" SelectionReadOnly="locked" ValueFormatter="Format" RowLabel="RowText" ColumnLabel="ColumnText" TotalText="All" EmptyText="No matching deliveries" OnCellSelected="Selected"/>
<p role="status">Last drilldown: @message</p>
@code {
 private bool rowTotals=true,columnTotals=true,locked,average,empty;
 private string message="none";
 private NgPivotRecord[] records=Data(1);
 private string Format(double? v)=>v.HasValue?v.Value.ToString("F1",System.Globalization.CultureInfo.InvariantCulture)+" credits":"No value";
 private string RowText(string v)=>v==""?"Unassigned":"Team "+v;
 private string ColumnText(string v)=>"Period "+v;
 private void Selected(NgPivotSelection p)=>message=$"{p.Row??"(total)"} / {p.Column??"(total)"} · {p.Records.Count} records · IDs: {string.Join(",",p.Records.Select(r=>r.Id))}";
 private static NgPivotRecord[] Data(int factor)=>[new("a",new Dictionary<string,string>{{"Team","A"},{"Quarter","Q1"}},10*factor),new("b",new Dictionary<string,string>{{"Team","A"},{"Quarter","Q1"}},30*factor),new("c",new Dictionary<string,string>{{"Team","B"},{"Quarter","Q2"}},90*factor),new("d",new Dictionary<string,string>{{"Team",""},{"Quarter","Q1"}},null)];
}

Flutter example

Dart / Flutter
bool rowTotals=true,columnTotals=true,locked=false,average=false,empty=false;
String message='none';
List<NgPivotRecord> records=data(1);
static List<NgPivotRecord> data(int factor)=>[NgPivotRecord('a',{'Team':'A','Quarter':'Q1'},10.0*factor),NgPivotRecord('b',{'Team':'A','Quarter':'Q1'},30.0*factor),NgPivotRecord('c',{'Team':'B','Quarter':'Q2'},90.0*factor),NgPivotRecord('d',{'Team':'','Quarter':'Q1'},null)];
Widget build(BuildContext context)=>Column(crossAxisAlignment:CrossAxisAlignment.stretch,children:[
 Wrap(spacing:8,runSpacing:8,children:[
  NgButton(onPressed:()=>setState(()=>rowTotals=!rowTotals),child:const Text('Toggle row totals')),
  NgButton(onPressed:()=>setState(()=>columnTotals=!columnTotals),child:const Text('Toggle column totals')),
  NgButton(onPressed:()=>setState(()=>locked=!locked),child:const Text('Toggle selection lock')),
  NgButton(onPressed:()=>setState(()=>average=!average),child:const Text('Toggle average')),
  NgButton(onPressed:()=>setState(()=>empty=!empty),child:const Text('Toggle empty data')),
  NgButton(onPressed:()=>setState(()=>records=data(2)),child:const Text('Update values')),
 ]),
 Text('Row totals ${rowTotals?"shown":"hidden"} · Column totals ${columnTotals?"shown":"hidden"} · Selection ${locked?"locked":"enabled"}'),
 NgPivotGrid(items:empty?const []:records,aggregate:average?NgAggregate.average:NgAggregate.sum,showRowTotals:rowTotals,showColumnTotals:columnTotals,selectionReadOnly:locked,
  valueFormatter:(v)=>v==null?'No value':'${v.toStringAsFixed(1)} credits',rowLabel:(v)=>v.isEmpty?'Unassigned':'Team $v',columnLabel:(v)=>'Period $v',totalText:'All',emptyText:'No matching deliveries',
  onCellSelected:(p)=>setState(()=>message='${p.row??"(total)"} / ${p.column??"(total)"} · ${p.records.length} records · IDs: ${p.records.map((r)=>r.id).join(",")}')),
 Text('Last drilldown: $message'),
]);

Blazor parameters and events

Names are case-sensitive. ? permits null. EditorRequired parameters are marked required; other values use the initializer or CLR default.

Parameter Type Default / requirement Purpose
Label string "Pivot table" Visible label or accessible name.
Items IReadOnlyList<NgPivotRecord> [] Input collection. Replace it when changing records or configuration.
RowField string "Team" See the behavior rules and model contract above.
ColumnField string "Quarter" See the behavior rules and model contract above.
Aggregate NgAggregate NgAggregate.Sum See the behavior rules and model contract above.
ShowRowTotals bool true See the behavior rules and model contract above.
ShowColumnTotals bool true See the behavior rules and model contract above.
SelectionReadOnly bool false See the behavior rules and model contract above.
ValueFormatter Func<double?,string>? not set See the behavior rules and model contract above.
RowLabel Func<string,string>? not set See the behavior rules and model contract above.
ColumnLabel Func<string,string>? not set See the behavior rules and model contract above.
TotalText string "Total" See the behavior rules and model contract above.
EmptyText string "No pivot data." See the behavior rules and model contract above.
OnCellSelected EventCallback<NgPivotSelection> not set See the behavior rules and model contract above.

Flutter parameters and events

required marks a mandatory argument even if its type is nullable. key is the standard Flutter Key. The exact constructor below specifies defaults.

Argument Type Purpose
label String Visible label or accessible name.
rowField String See the behavior rules and model contract above.
columnField String See the behavior rules and model contract above.
items List<NgPivotRecord> Input collection. Replace it when changing records or configuration.
aggregate NgAggregate See the behavior rules and model contract above.
showRowTotals bool See the behavior rules and model contract above.
showColumnTotals bool See the behavior rules and model contract above.
selectionReadOnly bool See the behavior rules and model contract above.
totalText String See the behavior rules and model contract above.
emptyText String See the behavior rules and model contract above.
valueFormatter String Function(double?)? See the behavior rules and model contract above.
rowLabel String Function(String)? See the behavior rules and model contract above.
columnLabel String Function(String)? See the behavior rules and model contract above.
onCellSelected ValueChanged<NgPivotSelection>? See the behavior rules and model contract above.
Dart / Flutter
const NgPivotGrid({
    super.key,
    this.label = 'Pivot table',
    required this.items,
    this.rowField = 'Team',
    this.columnField = 'Quarter',
    this.aggregate = NgAggregate.sum,
    this.onCellSelected,
    this.showRowTotals = true,
    this.showColumnTotals = true,
    this.selectionReadOnly = false,
    this.totalText = 'Total',
    this.emptyText = 'No pivot data.',
    this.valueFormatter,
    this.rowLabel,
    this.columnLabel,
  });

Integration and troubleshooting

If the UI does not update, confirm that the parent stores callback values and passes them back. Blazor event handlers need interactive rendering; Flutter requires setState or a state manager. Submit form data only after successful validation.

Check accessible labels, empty/invalid states, disabled actions and narrow layouts. Keyboard support is not full screen-reader certification. Current visual verification targets Web in visible Chrome on Linux/iMac; native mobile platforms are a separate phase.

Source and complete examples

API declarations are generated from current source. Curated examples and behavior live in scripts/manual_content.py; regenerate with python3 scripts/generate_manual.py.