# Example: The grouping section

The Grouping pane opens on load via defaultActiveSection. Add and reorder group-by levels, expand and collapse everything at once, flip hide-grouped-columns, and override a column's aggregate — with the drag-to-group strip reflecting every change.

Source: https://pretable.ai/examples/tool-panel-grouping.md

```tsx GroupingSectionGrid.tsx
"use client";

import { useState, type ComponentProps } from "react";

import { PretableSurface } from "@pretable/react";

import { columns } from "./columns";
import { holdings, type Holding } from "./data";

const VIEWPORT_HEIGHT = 380;

export function GroupingSectionGrid() {
  // The query is controlled only to seed one grouping level on load; the
  // setter hands every later write straight back, so the pane's group-by
  // list, the drag-to-group strip, and this prop stay one model.
  const [query, setQuery] = useState<
    NonNullable<ComponentProps<typeof PretableSurface<Holding>>["query"]>
  >({
    filters: [],
    sort: [],
    rowGroups: [{ columnId: "desk" }],
  });

  return (
    <div>
      <p style={{ margin: "0 0 8px", fontSize: 13 }}>
        The Grouping pane is open on load, with rows already grouped by{" "}
        <strong>Desk</strong>. <strong>+ Add group</strong> adds{" "}
        <strong>Sector</strong> as a second level; drag a grip to reorder the
        levels, and ✕ removes one — the strip above the header shows every
        change, because both are projections of one model. Flip{" "}
        <strong>Hide grouped columns</strong> to keep the grouped column in the
        body, and change <strong>Market value</strong>&apos;s aggregate — its{" "}
        <strong>Default (Sum)</strong> is the prop&apos;s choice, and{" "}
        <strong>None</strong> blanks the group row&apos;s cell without touching
        the prop.
      </p>
      <PretableSurface<Holding>
        ariaLabel="Holdings"
        columns={columns}
        getRowId={(row) => row.id}
        groupPanel={{ enabled: true }}
        onQueryChange={setQuery}
        query={query}
        rows={holdings}
        toolPanel={{ defaultActiveSection: "grouping" }}
        viewportHeight={VIEWPORT_HEIGHT}
      />
    </div>
  );
}
```

```ts columns.ts
import type { PretableColumn } from "@pretable/react";

import type { Holding } from "./data";

const usd = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
});

const count = new Intl.NumberFormat("en-US");

// `marketValue` declares `aggregate: "sum"`, so its picker in the pane opens
// on `Default (Sum)`; `quantity` declares nothing, so its picker opens on
// `Default (None)` — the difference between "the prop's choice" and "an
// override" is the thing the pane makes visible.
export const columns: PretableColumn<Holding>[] = [
  { id: "symbol", header: "Symbol", widthPx: 90 },
  { id: "desk", header: "Desk", type: "enum", widthPx: 110 },
  { id: "sector", header: "Sector", type: "enum", widthPx: 120 },
  {
    id: "quantity",
    header: "Qty",
    type: "number",
    widthPx: 90,
    format: ({ value }) => count.format(value as number),
    // NOT `format`. `PretableFormatInput.row` is non-optional and a group row
    // has no row behind it, so the cell formatter above is not a legal
    // aggregate formatter — this is the hook that is.
    formatAggregate: ({ value }) =>
      typeof value === "number" ? count.format(value) : "",
  },
  {
    id: "marketValue",
    header: "Market value",
    type: "number",
    widthPx: 120,
    aggregate: "sum",
    format: ({ value }) => usd.format(value as number),
    formatAggregate: ({ value }) =>
      typeof value === "number" ? usd.format(value) : "",
  },
];
```

```ts data.ts
export interface Holding {
  id: string;
  symbol: string;
  desk: string;
  sector: string;
  quantity: number;
  price: number;
  marketValue: number;
}

export const holdings: Holding[] = [
  {
    id: "h1",
    symbol: "NVDA",
    desk: "Equities",
    sector: "Technology",
    quantity: 4200,
    price: 122,
    marketValue: 512_400,
  },
  {
    id: "h2",
    symbol: "MSFT",
    desk: "Equities",
    sector: "Technology",
    quantity: 1800,
    price: 416,
    marketValue: 748_900,
  },
  {
    id: "h3",
    symbol: "LLY",
    desk: "Equities",
    sector: "Healthcare",
    quantity: 620,
    price: 784,
    marketValue: 486_100,
  },
  {
    id: "h4",
    symbol: "UNH",
    desk: "Equities",
    sector: "Healthcare",
    quantity: 950,
    price: 528,
    marketValue: 501_300,
  },
  {
    id: "h5",
    symbol: "XOM",
    desk: "Equities",
    sector: "Energy",
    quantity: 3100,
    price: 118,
    marketValue: 364_800,
  },
  {
    id: "h6",
    symbol: "JPM",
    desk: "Credit",
    sector: "Financials",
    quantity: 2400,
    price: 260,
    marketValue: 623_500,
  },
  {
    id: "h7",
    symbol: "GS",
    desk: "Credit",
    sector: "Financials",
    quantity: 780,
    price: 538,
    marketValue: 419_700,
  },
  {
    id: "h8",
    symbol: "CVX",
    desk: "Credit",
    sector: "Energy",
    quantity: 1500,
    price: 159,
    marketValue: 238_200,
  },
  {
    id: "h9",
    symbol: "TLT",
    desk: "Macro",
    sector: "Financials",
    quantity: 5600,
    price: 89,
    marketValue: 497_800,
  },
  {
    id: "h10",
    symbol: "USO",
    desk: "Macro",
    sector: "Energy",
    quantity: 8800,
    price: 70,
    marketValue: 611_600,
  },
  {
    id: "h11",
    symbol: "SMH",
    desk: "Macro",
    sector: "Technology",
    quantity: 1250,
    price: 264,
    marketValue: 329_400,
  },
  {
    id: "h12",
    symbol: "QQQ",
    desk: "Macro",
    sector: "Technology",
    quantity: 900,
    price: 503,
    marketValue: 452_700,
  },
];
```
