You can use any field available in the DataSource to do the grouping - it can even be a field that is not a column.

Note

When using TypeScript, both DataSource and InfiniteTable components are generic and need to be rendered/instantiated with a DATA_TYPE parameter. The fields in that DATA_TYPE can then be used for grouping.

type Person = {
  name: string;
  age: number;
  country: string;
  id: string;
}

const groupBy = [{field: 'country'}]

<DataSource<Person> groupBy={groupBy}>
  <InfiniteTable<Person> />
</DataSource>

In the example above, we’re grouping by country, which is a field available in the Person type. Specifying a field not defined in the Person type would be a type error.

Additionally, a column object can be used together with the field to define how the group column should be rendered.

const groupBy = [
  {
    field: 'country',
    column: {
      width: 150,
      header: 'Country group',
    }
  }
]

The example below puts it all together.

Also see the groupBy API reference to find out more.

Simple row grouping
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  DataSourcePropGroupBy,
} from '@infinite-table/infinite-react';

import { columns, Employee } from './columns';

const groupBy: DataSourcePropGroupBy<Employee> = [
  {
    field: 'country',
    column: {
      header: 'Country group',
      renderValue: ({ value }) => <>Country: {value}</>,
    },
  },
];

export default function App() {
  return (
    <DataSource<Employee>
      data={dataSource}
      primaryKey="id"
      groupBy={groupBy}>
      <InfiniteTable<Employee> columns={columns} />
    </DataSource>
  );
}

const dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + '/employees1k'
  )
    .then((r) => r.json())
    .then((data: Employee[]) => data);
};

In groupBy.column you can use any column property - so, for example, you can define a custom renderValue function to customize the rendering.

const groupBy = [
  {
    field: 'country',
    column: {
      renderValue: ({ value }) => <>Country: {value}</>,
    }
  }
]

Grouping strategies

When you want to group by multiple fields, InfiniteTable offers multiple grouping strategies:

  • multi column mode - the default.
  • single column mode
  • inline mode

You can specify the rendering strategy by setting the groupRenderStrategy property to any of the following: multi-column, single-column or inline.

Multiple groups columns

When grouping by multiple fields, by default the component will render a group column for each group field

const groupBy = [
  {
    field: 'age',
    column: {
      width: 100,      
      renderValue: ({ value }) => <>Age: {value}</>,
    }
  },
  {
    field: 'companyName'
  },
  {
    field: 'country'
  }
]

Let’s see an example of how the component would render the table with the multi-column strategy.

Multi-column group render strategy
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  DataSourcePropGroupBy,
} from '@infinite-table/infinite-react';
import { columns, Employee } from './columns';

const groupBy: DataSourcePropGroupBy<Employee> = [
  {
    field: 'age',
    column: {
      renderValue: ({ value }: { value: any }) =>
        `Age: ${value}`,
    },
  },
  {
    field: 'companyName',
  },
];

export default function App() {
  return (
    <DataSource<Employee>
      data={dataSource}
      primaryKey="id"
      groupBy={groupBy}>
      <InfiniteTable<Employee>
        columns={columns}
        columnDefaultWidth={150}
      />
    </DataSource>
  );
}

const dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + '/employees10k'
  )
    .then((r) => r.json())
    .then((data: Employee[]) => data);
};

For the multi-column strategy, you can use hideEmptyGroupColumns in order to hide columns for groups which are currently not visible.

Hide Empty Group Columns
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  DataSourcePropGroupBy,
  InfiniteTablePropGroupRenderStrategy,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import { columns, Employee } from './employee-columns';

const groupBy: DataSourcePropGroupBy<Employee> = [
  {
    field: 'department',
  },
  {
    field: 'companyName',
  },
];

const domProps = {
  style: { flex: 1 },
};

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

export default function App() {
  const [groupRenderStrategy, setGroupRenderStrategy] =
    React.useState<InfiniteTablePropGroupRenderStrategy>(
      'multi-column'
    );

  const [hideEmptyGroupColumns, setHideEmptyGroupColumns] =
    React.useState(true);
  return (
    <div
      style={{
        display: 'flex',
        flex: 1,
        color: 'var(--infinite-row-color)',
        flexFlow: 'column',
        background: 'var(--infinite-background)',
      }}>
      <div style={{ padding: 10 }}>
        <label>
          <input
            type="checkbox"
            checked={hideEmptyGroupColumns}
            onChange={() => {
              setHideEmptyGroupColumns(
                !hideEmptyGroupColumns
              );
            }}
          />
          Hide Empty Group Columns (make sure all
          `Department` groups are collapsed to see it in
          action)
        </label>
      </div>
      <DataSource<Employee>
        data={dataSource}
        primaryKey="id"
        defaultGroupRowsState={groupRowsState}
        groupBy={groupBy}>
        <InfiniteTable<Employee>
          domProps={domProps}
          hideEmptyGroupColumns={hideEmptyGroupColumns}
          groupRenderStrategy={'multi-column'}
          columns={columns}
          columnDefaultWidth={250}
        />
      </DataSource>
    </div>
  );
}

const dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' +
      '/employees10'
  )
    .then((r) => r.json())
    .then((data: Employee[]) => data);
};

Gotcha

You can specify an id for group columns. This is helpful if you want to size those columns (via columnSizing) or pin them (via columnPinning) or configure them in other ways. If no id is specified, it will be generated like this: "group-by-${field}"

Single group column

You can group by multiple fields, yet only render a single group column. To choose this rendering strategy, specify groupRenderStrategy property to be single-column.

In this case, you can’t override the group column for each group field, as there’s only one group column being generated. However, you can specify a groupColumn property to customize the generated column.

Single-column group render strategy
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  DataSourcePropGroupBy,
  InfiniteTableColumnRenderValueParam,
} from '@infinite-table/infinite-react';
import { columns, Employee } from './columns';

const groupBy: DataSourcePropGroupBy<Employee> = [
  {
    field: 'age',
  },
  {
    field: 'companyName',
  },
];

const groupColumn = {
  header: 'Grouping',
  width: 250,
  // in this function we have access to collapsed info
  // and grouping info about the current row - see rowInfo.groupBy
  renderValue: ({
    value,
    rowInfo,
  }: InfiniteTableColumnRenderValueParam<Employee>) => {
    if (!rowInfo.isGroupRow) {
      return value
    }
    const groupBy = rowInfo.groupBy || [];
    const collapsed = rowInfo.collapsed;
    const groupField = groupBy[groupBy.length - 1];

    if (groupField === 'age') {
      return `🥳 ${value}${collapsed ? ' 🤷‍♂️' : ''}`;
    }

    return `🎉 ${value}`;
  },
};

export default function App() {
  return (
    <DataSource<Employee>
      data={dataSource}
      primaryKey="id"
      groupBy={groupBy}>
      <InfiniteTable<Employee>
        groupRenderStrategy="single-column"
        groupColumn={groupColumn}
        columns={columns}
        columnDefaultWidth={150}
      />
    </DataSource>
  );
}

const dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + '/employees10k'
  )
    .then((r) => r.json())
    .then((data: Employee[]) => data);
};

Note

If groupColumn is specified to an object and no groupRenderStrategy is passed, the render strategy will be single-column.

groupColumn can also be a function, which allows you to individually customize each group column - in case the `multi-column` strategy is used.

Gotcha

You can specify an id for the single group column by using groupColumn, as detailed above. This is helpful if you want to size this column (via columnSizing) or pin it (via columnPinning) or configure it in other ways. If no id is specified, it will default to "group-by".

Inline group column

When inline group rendering is used (groupRenderStrategy=“inline”), the columns bound to the corresponding group by fields are used for rendering, so no group columns are generated. This way of rendering groups is only recommended when you’re sure you have small groups (smaller than the number of rows visible in the viewport).

Aggregations

When grouping, you can also aggregate the values of the grouped rows. This is done via the DataSource.aggregationReducers=true property. See the example below

Grouping with aggregations
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  DataSourcePropGroupBy,
  InfiniteTableColumnRenderValueParam,
  DataSourcePropAggregationReducers,
  InfiniteTablePropColumns,
  DataSourceGroupBy,
  GroupRowsState,
} from '@infinite-table/infinite-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 avgReducer = {
  initialValue: 0,
  reducer: (acc: number, sum: number) => acc + sum,
  done: (value: number, arr: any[]) =>
    arr.length ? Math.floor(value / arr.length) : 0,
};
const aggregationReducers: DataSourcePropAggregationReducers<Developer> =
  {
    salary: {
      field: 'salary',

      ...avgReducer,
    },
    age: {
      field: 'age',
      ...avgReducer,
    },
  };

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },

  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};
const groupColumn = {
  header: 'Grouping',
  defaultWidth: 250,
  // in this function we have access to collapsed info
  // and grouping info about the current row - see rowInfo.groupBy
  renderValue: ({
    value,
    rowInfo,
  }: InfiniteTableColumnRenderValueParam<Developer>) => {
    if (!rowInfo.isGroupRow) {
      return value
    }
    const groupBy = rowInfo.groupBy || [];
    const collapsed = rowInfo.collapsed;
    const groupField = groupBy[groupBy.length - 1];

    if (groupField === 'age') {
      return `🥳 ${value}${collapsed ? ' 🤷‍♂️' : ''}`;
    }

    return `🎉 ${value}`;
  },
};

const defaultGroupRowsState = new GroupRowsState({
  //make all groups collapsed by default
  collapsedRows: true,
  expandedRows: [],
});

export default function App() {
  const groupBy: DataSourceGroupBy<Developer>[] =
    React.useMemo(
      () => [
        {
          field: 'country',
        },
        { field: 'stack' },
      ],
      []
    );
  return (
    <DataSource<Developer>
      data={dataSource}
      primaryKey="id"
      defaultGroupRowsState={defaultGroupRowsState}
      aggregationReducers={aggregationReducers}
      groupBy={groupBy}>
      <InfiniteTable<Developer>
        groupRenderStrategy="single-column"
        groupColumn={groupColumn}
        columns={columns}
        columnDefaultWidth={150}
      />
    </DataSource>
  );
}

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

Each reducer from the aggregationReducers map can have the following properties:

  • field - the field to aggregate on
  • getter(data) - a value-getter function, if the aggregation values are are not mapped directly to a field
  • initialValue - the initial value to start with when computing the aggregation (for client-side aggregations only)
  • reducer: string | (acc, current)=>value - the reducer function to use when computing the aggregation (for client-side aggregations only). For server-side aggregations, this will be a string
  • done(value, arr) - a function that is called when the aggregation is done (for client-side aggregations only) and returns the final value of the aggregation
  • name - useful especially in combination with pivotBy, as it will be used as the pivot column header.

If an aggregation reducer is bound to a field in the dataset, and there is a column mapped to the same field, that column will show the corresponding aggregation value for each group row, as shown in the example above.

Gotcha

If you want to prevent the user to expand the last level of group rows, you can override the render function for the group column

Customized group expand on last group level
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  DataSourcePropAggregationReducers,
  InfiniteTablePropColumns,
  DataSourceGroupBy,
  GroupRowsState,
  InfiniteTableGroupColumnFunction,
  InfiniteTableGroupColumnBase,
  InfiniteTableColumnRenderParam,
} from '@infinite-table/infinite-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 avgReducer = {
  initialValue: 0,
  reducer: (acc: number, sum: number) => acc + sum,
  done: (value: number, arr: any[]) =>
    arr.length ? Math.floor(value / arr.length) : 0,
};
const aggregationReducers: DataSourcePropAggregationReducers<Developer> =
  {
    salary: {
      field: 'salary',

      ...avgReducer,
    },
    age: {
      field: 'age',
      ...avgReducer,
    },
  };

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },

  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

// TODO remove this after the next release
//@ts-ignore
const groupColumn: InfiniteTableGroupColumnFunction<
  Developer
> = (arg) => {
  const column = {} as Partial<
    InfiniteTableGroupColumnBase<Developer>
  >;

  if (arg.groupIndexForColumn === arg.groupBy.length - 1) {
    column.render = (
      param: InfiniteTableColumnRenderParam<Developer>
    ) => {
      const { value, rowInfo } = param;
      if (
        rowInfo.isGroupRow &&
        rowInfo.groupBy?.length !=
          rowInfo.rootGroupBy?.length
      ) {
        // we are on a group row that is the last grouping level
        return null;
      }
      return value;
    };
  }
  return column;
};

const defaultGroupRowsState = new GroupRowsState({
  //make all groups collapsed by default
  collapsedRows: true,
  expandedRows: [],
});

export default function App() {
  const groupBy: DataSourceGroupBy<Developer>[] =
    React.useMemo(
      () => [
        {
          field: 'country',
        },
        { field: 'stack' },
      ],
      []
    );
  return (
    <DataSource<Developer>
      data={dataSource}
      primaryKey="id"
      defaultGroupRowsState={defaultGroupRowsState}
      aggregationReducers={aggregationReducers}
      groupBy={groupBy}>
      <InfiniteTable<Developer>
        groupRenderStrategy="multi-column"
        groupColumn={groupColumn}
        columns={columns}
        columnDefaultWidth={250}
      />
    </DataSource>
  );
}

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

Server side grouping with lazy loading

Lazy loading becomes all the more useful when working with grouped data.

The DataSource data function is called with an object that has all the information about the current DataSource state(grouping/pivoting/sorting/lazy-loading, etc) - see the paragraphs above for details.

Server side grouping needs two kinds of data responses in order to work properly:

  • response for non-leaf row groups - these are groups that have children. For such groups (including the top-level group), the DataSource.data function must return a promise that’s resolved to an object with the following properties:
    • totalCount - the total number of records in the group
    • data - an array of objects that describes non-leaf child groups, each object has the following properties:
      • keys - an array of the group keys (usually strings) that uniquely identifies the group, from the root to the current group
      • data - an object that describes the common properties of the group
      • aggregations - an object that describes the aggregations for the current group
  • response for leaf rows - these are normal rows - rows that would have been served in the non-grouped response. The resolved object should have the following properties:
    • data - an array of objects that describes the rows
    • totalCount - the total number of records on the server, that are part of the current group

Here’s an example, that assumes grouping by country and city and aggregations by age and salary (average values):

//request:
groupKeys: [] // empty keys array, so it's a top-level group
groupBy: [{"field":"country"},{"field":"city"}]
reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}]
// lazyLoadStartIndex: 0, - passed if lazyLoad is configured with a batchSize
// lazyLoadBatchSize: 20 - passed if lazyLoad is configured with a batchSize

//response
{
  cache: true,
  totalCount: 20,
  data: [
    {
      data: {country: "Argentina"},
      aggregations: {avgSalary: 20000, avgAge: 30},
      keys: ["Argentina"],
    },
    {
      data: {country: "Australia"},
      aggregations: {avgSalary: 25000, avgAge: 35},
      keys: ["Australia"],
    }
    //...
  ]
}

Now let’s expand the first group and see how the request/response would look like:


//request:
groupKeys: ["Argentina"]
groupBy: [{"field":"country"},{"field":"city"}]
reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}]

//response
{
  totalCount: 4,
  data: [
    {
      data: {country: "Argentina", city: "Buenos Aires"},
      aggregations: {avgSalary: 20000, avgAge: 30},
      keys: ["Argentina", "Buenos Aires"],
    },
    {
      data: {country: "Argentina", city: "Cordoba"},
      aggregations: {avgSalary: 25000, avgAge: 35},
      keys: ["Argentina", "Cordoba"],
    },
    //...
  ]
}

Finally, let’s have a look at the leaf/normal rows and a request for them:


//request
groupKeys: ["Argentina","Buenos Aires"]
groupBy: [{"field":"country"},{"field":"city"}]
reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}]

//response
{
  totalCount: 20,
  data: [
    {
      id: 34,
      country: "Argentina",
      city: "Buenos Aires",
      age: 30,
      salary: 20000,
      stack: "full-stack",
      firstName: "John",
      //...
    },
    {
      id: 35,
      country: "Argentina",
      city: "Buenos Aires",
      age: 35,
      salary: 25000,
      stack: "backend",
      firstName: "Jane",
      //...
    },
    //...
  ]
}

Note

When a row group is expanded, since InfiniteTable has the group keys from the previous response when the node was loaded, it will use the keys array and pass them to the DataSource.data function when requesting for the children of the respective group.

You know when to serve last-level rows, because in that case, the length of the groupKeys array will be equal to the length of the groupBy array.

Server side grouping with lazy loding
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
  InfiniteTablePropColumns,
  GroupRowsState,
  DataSourceGroupBy,
  DataSourcePropAggregationReducers,
} from '@infinite-table/infinite-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 aggregationReducers: DataSourcePropAggregationReducers<Developer> =
  {
    salary: {
      name: 'Salary (avg)',
      field: 'salary',
      reducer: 'avg',
    },
    age: {
      name: 'Age (avg)',
      field: 'age',
      reducer: 'avg',
    },
  };

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },

  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

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

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

  return (
    <DataSource<Developer>
      primaryKey="id"
      data={dataSource}
      groupBy={groupBy}
      aggregationReducers={aggregationReducers}
      defaultGroupRowsState={groupRowsState}
      lazyLoad={true}>
      <InfiniteTable<Developer>
        scrollStopDelay={10}
        hideEmptyGroupColumns
        columns={columns}
        columnDefaultWidth={220}
      />
    </DataSource>
  );
}

const dataSource: DataSourceData<Developer> = ({
  pivotBy,
  aggregationReducers,
  groupBy,

  lazyLoadStartIndex,
  lazyLoadBatchSize,
  groupKeys = [],
  sortInfo,
}) => {
  if (sortInfo && !Array.isArray(sortInfo)) {
    sortInfo = [sortInfo];
  }
  const startLimit: string[] = [];
  if (lazyLoadBatchSize && lazyLoadBatchSize > 0) {
    const start = lazyLoadStartIndex || 0;
    startLimit.push(`start=${start}`);
    startLimit.push(`limit=${lazyLoadBatchSize}`);
  }
  const args = [
    ...startLimit,
    pivotBy
      ? 'pivotBy=' +
        JSON.stringify(
          pivotBy.map((p) => ({ field: p.field }))
        )
      : null,
    `groupKeys=${JSON.stringify(groupKeys)}`,
    groupBy
      ? 'groupBy=' +
        JSON.stringify(
          groupBy.map((p) => ({ field: p.field }))
        )
      : null,
    sortInfo
      ? 'sortInfo=' +
        JSON.stringify(
          sortInfo.map((s) => ({
            field: s.field,
            dir: s.dir,
          }))
        )
      : null,
    aggregationReducers
      ? 'reducers=' +
        JSON.stringify(
          Object.keys(aggregationReducers).map((key) => ({
            field: aggregationReducers[key].field,
            id: key,
            name: aggregationReducers[key].reducer,
          }))
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' +
      `/developers30k-sql?` +
      args
  ).then((r) => r.json());
};

Eager loading for group row nodes

When using lazy-loading together with batching, node data (without children) is loaded when a node (normal or grouped) comes into view. Only when a group node is expanded will its children be loaded. However, you can do this loading eagerly, by using the dataset property on the node you want to load.

Note

This can be useful in combination with using dataParams.groupRowsState from the data function - so your datasource can know which groups are expanded, and thus it can serve those groups already loaded with children.

//request:
groupKeys: [] // empty keys array, so it's a top-level group
groupBy: [{"field":"country"},{"field":"city"}]
reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}]
// lazyLoadStartIndex: 0, - passed if lazyLoad is configured with a batchSize
// lazyLoadBatchSize: 20 - passed if lazyLoad is configured with a batchSize

//response
{
  cache: true,
  totalCount: 20,
  data: [
    {
      data: {country: "Argentina"},
      aggregations: {avgSalary: 20000, avgAge: 30},
      keys: ["Argentina"],
      // NOTE this dataset property used for eager-loading of group nodes
      dataset: {
        // the shape of the dataset is the same as the one normally returned by the datasource
        cache: true,
        totalCount: 4,
        data: [
          {
            data: {country: "Argentina", city: "Buenos Aires"},
            aggregations: {avgSalary: 20000, avgAge: 30},
            keys: ["Argentina", "Buenos Aires"],
          },
          {
            data: {country: "Argentina", city: "Cordoba"},
            aggregations: {avgSalary: 25000, avgAge: 35},
            keys: ["Argentina", "Cordoba"],
          },
        ]
      }
    },
    {
      data: {country: "Australia"},
      aggregations: {avgSalary: 25000, avgAge: 35},
      keys: ["Australia"],
    }
    //...
  ]
}