FreeGridv1.15.1한국어

Merge rows by columns

Load raw rows and merge them by columns: hide or remove one of the by columns and rows that agree on the rest become one, with sums, averages, min, max or counts.

Live demoMerge rows by columns

Load raw rows, then hide a column to merge the rows that agree on the rest, with sums and averages.

Code for this demo
import { createGrid, type ColumnDef } from '@freegridjs/core';
import { makeRows } from '../data';
import { button, separator, text, tr, type DemoContext } from '../ui';

export default function mount(ctx: DemoContext) {
  const { lang } = ctx;
  const h = (en: string, ko: string) => tr(lang, { en, ko });
  const money = { type: 'number', format: '#,##0', width: 120 } as const;
  const columns: ColumnDef[] = [
    { field: 'id', header: 'ID', type: 'number', format: '0', width: 80 },
    { field: 'country', header: h('Country', '국가'), width: 140 },
    { field: 'name', header: h('Name', '이름'), width: 160 },
    { field: 'company', header: h('Company', '회사'), width: 130 },
    { field: 'email', header: h('Email', '이메일'), width: 220 },
    { field: 'q1', header: h('Q1', '1분기'), ...money, aggFunc: 'sum' },
    { field: 'q2', header: h('Q2', '2분기'), ...money, aggFunc: 'sum' },
    { field: 'total', header: h('Annual revenue', '연매출'), ...money, width: 130, aggFunc: 'avg' },
  ];
  const grid = createGrid(ctx.host, {
    columns,
    data: makeRows(300),
    locale: lang,
    rollup: { by: ['country', 'name', 'company'] },
    summary: { rows: [{ label: h('Total', '합계'), columns: { q1: 'sum', q2: 'sum', total: 'avg' } }] },
  });
  grid.on('columnVisible', e => ctx.log(`${e.colId} ${e.visible ? h('shown', '보임') : h('hidden', '숨김')} → ${grid.getDisplayedRowCount()} ${h('rows', '행')}`));

  const show = (ids: string[], on: boolean) => ids.forEach(id => grid.setColumnVisible(id, on));
  text(ctx, h('Every row shows at first. Hide Name and rows with the same country and company merge: quarters are summed, annual revenue averaged. The column menu of a number column picks the aggregate.',
    '처음에는 모든 행이 보입니다. 이름을 숨기면 국가·회사가 같은 행끼리 합쳐져 분기는 합계, 연매출은 평균이 됩니다. 숫자 컬럼의 메뉴에서 집계 방식을 고를 수 있습니다.'));
  separator(ctx);
  button(ctx, h('Hide Name', '이름 숨기기'), () => show(['name'], false));
  button(ctx, h('Hide Name and Company', '이름·회사 숨기기'), () => show(['name', 'company'], false));
  button(ctx, h('Show all', '모두 보이기'), () => show(['name', 'company'], true));
  separator(ctx);
  button(ctx, h('Annual revenue: max', '연매출: 최대'), () => grid.setColumnAggFunc('total', 'max'));
  button(ctx, h('Annual revenue: average', '연매출: 평균'), () => grid.setColumnAggFunc('total', 'avg'));
  return grid;
}

Load the raw rows, name the columns the rows merge by, and the grid shows every row at first. Hide or remove one of those columns and the rows that agree on the rest become one row: with rollup: { by: ['country', 'name', 'company'] }, hiding Name leaves one row per country and company. Show Name again and the rows split back. This is different from row grouping, which keeps every row and adds group rows above them; here the rows themselves merge, so the grid has fewer rows.

Columns with an aggFunc show the aggregate of the merged rows: 'sum', 'avg', 'min', 'max', 'count' or your own function. Change it with setColumnAggFunc() or from the aggregate entries in the column menu. Any other column, such as an ID or an email, shows its value when every merged row has the same one and stays empty otherwise; it never keeps rows apart. Only the by columns that are shown count, whether they are hidden with the column menu, the column chooser, the side bar, setColumnVisible(), or removed with setColumns(). Rows that agree on every shown by column are merged from the start.

The merged rows can be sorted, filtered, grouped, exported and charted. Group rows and summary rows are calculated from the original rows, so averages stay correct. Merged rows cannot be edited, and getData() still returns the original rows. setRollup({ by }) changes the columns, and setRollup(false) shows the original rows again. With rollup: true, every shown column without an aggFunc is a by column. Rollup works with rows in the browser, not with a datasource or tree data, and a pivot table takes precedence while it is shown.

With rowSelection, checking a merged row means checking the original rows it merges. getSelectedRows() and the selectionChange event return those original rows (the objects in your data, so their ids can go to a server), and removeSelectedRows(), removeRows() given a merged row, and Delete rows in the context menu remove all of them; undo puts them back. Copying the checked rows and exportCsv({ rows: 'selected' }) give the merged rows as shown. getState().selected holds the ids of the rows as shown: a merged row has the id of its first original row. When the rows merge again (a column is hidden or shown, the data changes, rollup is turned on or off), the selection follows the original rows: a new merged row is checked only when every row it merges was checked, so a delete never reaches a row nobody checked.

// Raw rows in; the rows merge by the by columns that are shown
const grid = createGrid(el, {
  columns: [
    { field: 'id' }, { field: 'country' }, { field: 'name' }, { field: 'company' }, { field: 'email' },
    { field: 'sales', type: 'number', aggFunc: 'sum' },
    { field: 'price', type: 'number', aggFunc: 'avg' },   // 'sum' | 'avg' | 'min' | 'max' | 'count' | (values, rows) => …
  ],
  data,
  rollup: { by: ['country', 'name', 'company'] },        // every row shows at first
});

grid.setColumnVisible('name', false);     // one row per country + company: sales summed, price averaged,
                                          // id and email shown only where the merged rows share them
grid.setColumnVisible('name', true);      // the rows split back
grid.setColumnAggFunc('price', 'max');    // the column menu has the same choices
grid.setRollup({ by: ['country'] });      // other by columns
grid.setRollup(false);                    // the original rows