Customizing generated pivot columns in a React DataGrid

By raduΒ·

View as Markdown
Generated pivot columns are the point of pivoting. You describe your grouping, your pivot columns and your aggregations and Infinite Table builds the report surface from the values in the data.
That first render is rarely the last one you want. Headers are generated based on values and will probably need customization, numbers are unformatted, styling is absent. The pivoting overview and the earlier post on building pivoted DataGrids cover how columns get generated. This article is about making those columns look and behave like the rest of your grid.
You do not customize each generated id by hand. You describe overrides at three scopes, and you inherit from the columns you already defined.

Three scopes, same column shape#

Pass a column object β€” or a function that returns one β€” in one of these places:
  1. pivotColumn on <InfiniteTable /> β€” every generated pivot column
  2. pivotBy.column β€” all columns in the group generated for that pivot field
  3. aggregationReducers.pivotColumn β€” only columns generated for that aggregation
The object has the same shape as a normal column. Header, width, style, defaultSortable, renderValue β€” anything you would set on a regular column.
Pick the layer that matches the question:
If the same property is set in more than one place, pivotColumn is applied last. aggregationReducers.pivotColumn wins over pivotBy.column. Inherited source-column props are the baseline β€” any explicit override wins.

Global defaults: width and sorting#

pivotColumn is the simplest layer. Generated pivot columns set defaultSortable to false, so columnDefaultSortable does not turn sorting on. Put defaultSortable: true on pivotColumn instead.
<InfiniteTable
  pivotColumn={{
    defaultSortable: true,
    defaultWidth: 180,
  }}
/>
That applies to leaf columns and totals. When totals should stay unsortable, pass a function. The generated column includes pivotTotalColumn:
<InfiniteTable
  pivotColumn={({ column }) => ({
    defaultSortable: !column.pivotTotalColumn,
  })}
/>
The sort type is taken from the original columns bound to the aggregation field β€” not the column id. A column defined as stargazers: { field: 'stargazers_count', type: 'number' } is enough for numeric sorting on the generated stargazers_count:true column.
The table starts sorted by the group column. pivotColumn={{ defaultSortable: true }} makes every generated pivot column sortable. Click a country / designer header to add a sort by that aggregated value.
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
  DataSourcePropSortInfo,
} from '@infinite-table/infinite-react';
import type {
  InfiniteTableColumnAggregator,
  InfiniteTablePropColumns,
  DataSourcePropAggregationReducers,
  DataSourceGroupBy,
  DataSourcePivotBy,
} from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource = () => {
  return fetch('https://data.infinite-table.com' + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const avgReducer: InfiniteTableColumnAggregator<Developer, any> = {
  initialValue: 0,
  field: 'salary',
  reducer: (acc, sum) => acc + sum,
  done: (sum, arr) => (arr.length ? sum / arr.length : 0),
};

const reducers: DataSourcePropAggregationReducers<Developer> = {
  salary: avgReducer,
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id' },
  firstName: { field: 'firstName' },
  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
  country: { field: 'country' },
  canDesign: { field: 'canDesign' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  age: { field: 'age' },
  salary: { field: 'salary', type: 'number' },
  currency: { field: 'currency' },
};

const groupRowsState = new GroupRowsState({
  expandedRows: [],
  collapsedRows: true,
});

const defaultSortInfo: DataSourcePropSortInfo<Developer> = [
  {
    id: 'group-by',
    dir: 1,
  },
];

export default function PivotSortingExample() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'preferredLanguage',
      },
      { field: 'stack' },
    ],
    [],
  );

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [{ field: 'country' }, { field: 'canDesign' }],
    [],
  );

  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        groupBy={groupBy}
        pivotBy={pivotBy}
        aggregationReducers={reducers}
        defaultGroupRowsState={groupRowsState}
        defaultSortInfo={defaultSortInfo}
      >
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              debugId="pivoting-sorting-example"
              columns={columns}
              pivotColumn={{
                defaultSortable: true,
              }}
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              columnDefaultWidth={200}
              groupRenderStrategy="single-column"
              pivotTotalColumnPosition="end"
              multiSortBehavior="append"
            />
          );
        }}
      </DataSource>
    </>
  );
}

Per pivot field: headers people recognize#

Raw pivot keys rarely belong in a header. Inventory statuses (in_stock, reserved), booleans (yes / no), and channel codes all need labels.
pivotBy.column as an object applies to every column in that field's group:
const pivotBy = [
  { field: 'country' },
  {
    field: 'canDesign',
    column: {
      defaultWidth: 160,
    },
  },
];
As a function, it receives the generated column, so you can vary the header per value. column.pivotGroupKey is the key at that pivot level:
const pivotBy = [
  { field: 'country' },
  {
    field: 'canDesign',
    column: ({ column }) => ({
      header: column.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer',
    }),
  },
];
The canDesign pivot values are rewritten to Designer / Non-designer. Country groups stay as they are.
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  DataSourcePropAggregationReducers,
  InfiniteTableColumnAggregator,
  InfiniteTablePropColumns,
  DataSourceGroupBy,
  DataSourcePivotBy,
} from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource = () => {
  return fetch('https://data.infinite-table.com' + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const avgReducer: InfiniteTableColumnAggregator<Developer, any> = {
  initialValue: 0,
  field: 'salary',
  reducer: (acc, sum) => acc + sum,
  done: (sum, arr) => (arr.length ? sum / arr.length : 0),
};

const columnAggregations: DataSourcePropAggregationReducers<Developer> = {
  salary: avgReducer,
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id' },
  firstName: { field: 'firstName' },
  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
  country: { field: 'country' },
  canDesign: { field: 'canDesign' },
  hobby: { field: 'hobby' },

  city: { field: 'city' },
  age: { field: 'age' },
  salary: { field: 'salary', type: 'number' },
  currency: { field: 'currency' },
};

const groupRowsState = new GroupRowsState({
  expandedRows: [],
  collapsedRows: true,
});

export default function PivotByExample() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'preferredLanguage',
      },
      { field: 'stack' },
    ],
    [],
  );

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [
      { field: 'country' },
      {
        field: 'canDesign',
        column: ({ column: pivotCol }) => {
          const lastKey =
            pivotCol.pivotGroupKeys[pivotCol.pivotGroupKeys.length - 1];

          return {
            header: lastKey === 'yes' ? 'πŸ’… Designer' : 'πŸ’» Non-designer',
          };
        },
      },
    ],
    [],
  );

  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        groupBy={groupBy}
        pivotBy={pivotBy}
        aggregationReducers={columnAggregations}
        defaultGroupRowsState={groupRowsState}
      >
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              debugId="pivoting-customize-column-example"
              columns={columns}
              hideEmptyGroupColumns
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              columnDefaultWidth={180}
            />
          );
        }}
      </DataSource>
    </>
  );
}
The same callback is where you would map warehouse statuses to β€œAvailable” / β€œReserved” / β€œQuarantine”, or support channels to the names operators already use. New warehouses or channels still become new columns β€” only the label changes.

Per aggregation: configure one measure#

When the grid shows more than one aggregation, you usually do not want the same config on every generated column. A salary average should look like a number column. A ticket-count aggregation should not inherit currency formatting.
Each reducer can define aggregationReducers.pivotColumn. That object applies only to columns generated for that aggregation.
const aggregationReducers = {
  salary: {
    field: 'salary',
    initialValue: 0,
    reducer: (acc, value) => acc + value,
    pivotColumn: {
      defaultSortable: true,
      type: 'number',
    },
  },
  license: {
    field: 'license',
    initialValue: 0,
    reducer: (acc) => acc + 1,
  },
};
Only the salary pivot columns become sortable. The license count columns stay with the generated defaults.
This is also the right place to set a reducer-specific header, defaultWidth, or style when you have several measures under the same pivot group (for example salary and headcount under each country).

Inherit from the columns you already wrote#

Pivoting is aggregations. Each reducer usually has a field. If <InfiniteTable /> already has a column bound to that same field, the generated pivot column inherits that column's configuration β€” type, style, header, renderValue, default width.
const columns = {
  salary: {
    field: 'salary',
    type: 'number',
    style: { color: 'red' },
  },
};

const aggregationReducers = {
  avgSalary: {
    field: 'salary',
    reducer: 'avg',
  },
};
You do not re-declare number formatting for every generated United States / yes salary column. Inheritance looks up the original column by field, not by column id, so this also works:
const columns = {
  stargazers: {
    field: 'stargazers_count',
    type: 'number',
  },
};
  • a string β€” inherit from that column id instead
  • false β€” inherit from none
  • true or omitted β€” inherit from the column bound to the aggregator's field (the default)
const aggregationReducers = {
  avgSalary: { field: 'salary', ...avgReducer },
  avgAge: {
    field: 'age',
    ...avgReducer,
    pivotColumn: {
      inheritFromColumn: 'preferredLanguage',
      defaultWidth: 500,
    },
  },
};
avgAge still aggregates age, but the generated columns pick up the preferredLanguage column config, then apply defaultWidth: 500 on top.
avgSalary inherits the red number styling from the salary column. avgAge inherits from firstName via inheritFromColumn and uses a wider default width.
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  DataSourcePropAggregationReducers,
  InfiniteTableColumnAggregator,
  InfiniteTablePropColumns,
  DataSourceGroupBy,
  DataSourcePivotBy,
} from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource = () => {
  return fetch('https://data.infinite-table.com' + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const avgReducer: InfiniteTableColumnAggregator<Developer, any> = {
  initialValue: 0,
  reducer: (acc, sum) => acc + sum,
  done: (sum, arr) => (arr.length ? sum / arr.length : 0),
};

const columnAggregations: DataSourcePropAggregationReducers<Developer> = {
  avgSalary: {
    field: 'salary',
    name: 'Average salary',
    ...avgReducer,
  },
  avgAge: {
    field: 'age',
    ...avgReducer,
    pivotColumn: {
      defaultWidth: 500,
      inheritFromColumn: 'firstName',
    },
  },
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id' },
  firstName: {
    field: 'firstName',
    style: {
      fontWeight: 'bold',
    },
    renderValue: ({ value }) => <>{value}!</>,
  },
  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
  country: { field: 'country' },
  canDesign: { field: 'canDesign' },
  hobby: { field: 'hobby' },

  city: { field: 'city' },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
    header: 'Salary',
    style: { color: 'red' },
  },
  currency: { field: 'currency' },
};

const groupRowsState = new GroupRowsState({
  expandedRows: [],
  collapsedRows: true,
});

export default function PivotByExample() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'preferredLanguage',
      },
      { field: 'stack' },
    ],
    [],
  );

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [
      { field: 'country' },
      {
        field: 'canDesign',
      },
    ],
    [],
  );

  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        groupBy={groupBy}
        pivotBy={pivotBy}
        aggregationReducers={columnAggregations}
        defaultGroupRowsState={groupRowsState}
      >
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              debugId="pivot-column-inherit-example"
              columns={columns}
              hideEmptyGroupColumns
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              columnDefaultWidth={180}
            />
          );
        }}
      </DataSource>
    </>
  );
}
Use inheritFromColumn: false when the source column's renderValue or style would be wrong on an aggregated cell β€” for example a name renderer on a numeric average.

Combining layers#

A typical analytics grid uses more than one layer:
const pivotBy = [
  { field: 'country' },
  {
    field: 'canDesign',
    column: ({ column }) => ({
      header: column.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer',
    }),
  },
];

const aggregationReducers = {
  salary: {
    field: 'salary',
    reducer: 'avg',
    pivotColumn: {
      defaultSortable: true,
    },
  },
  headcount: {
    initialValue: 0,
    reducer: (acc) => acc + 1,
    pivotColumn: {
      inheritFromColumn: false,
      header: 'Count',
    },
  },
};

<InfiniteTable
  pivotColumn={{
    defaultWidth: 140,
  }}
/>
Shared width comes from pivotColumn. Designer labels come from pivotBy.column. Only salary columns are sortable; headcount opts out of inheritance so it does not pick up salary formatting.
That is the whole customization model: generate columns from data, inherit what you already configured, then override at the scope that matches the change.

Go deeper in the docs#