# Example: The filters section

The Filters pane opens on load via defaultActiveSection. Add a condition, nest a group, flip a list between and and or, and watch the row count follow — then open a header funnel onto the same tree.

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

```tsx FilterBuilderGrid.tsx
"use client";

import { PretableSurface, type PretableTelemetry } from "@pretable/react";
import { useCallback, useState } from "react";

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

const VIEWPORT_HEIGHT = 380;

export function FilterBuilderGrid() {
  const [counts, setCounts] = useState({
    shown: holdings.length,
    total: holdings.length,
  });

  // `rowModelRowCount` is the post-filter row count; `visibleRowCount` would
  // be the viewport's, which changes when you scroll and says nothing about
  // the filter. Set through a value comparison so a telemetry publication
  // that did not move either number cannot re-render.
  const onTelemetryChange = useCallback((telemetry: PretableTelemetry) => {
    setCounts((current) =>
      current.shown === telemetry.rowModelRowCount &&
      current.total === telemetry.totalRowCount
        ? current
        : {
            shown: telemetry.rowModelRowCount,
            total: telemetry.totalRowCount,
          },
    );
  }, []);

  return (
    <div>
      <p style={{ margin: "0 0 8px", fontSize: 13 }}>
        The Filters pane is open on load. <strong>+ filter</strong> adds a
        condition on the first column — switch its picker to{" "}
        <strong>Sector</strong>, then open that header&apos;s funnel: the filter
        you just built is already in it, and one written there appears here.{" "}
        <strong>+ group</strong> nests a list, and the <strong>and</strong> /{" "}
        <strong>or</strong> button sets the connective for that whole list. A
        filter inside a group has no funnel to appear in — a funnel addresses
        only a top-level row.
      </p>
      {/*
        The query is deliberately uncontrolled — the panel writes filters
        straight into the engine, and the funnel writes into the same tree.
        Owning `query` here would put a third writer in the loop for no gain.
      */}
      <PretableSurface<Holding>
        ariaLabel="Holdings"
        columns={columns}
        getRowId={(row) => row.id}
        onTelemetryChange={onTelemetryChange}
        rows={holdings}
        toolPanel={{ defaultActiveSection: "filters" }}
        viewportHeight={VIEWPORT_HEIGHT}
      />
      <p style={{ margin: "8px 0 0", fontSize: 13 }}>
        Showing <code data-testid="filtered-row-count">{counts.shown}</code> of{" "}
        <code data-testid="total-row-count">{counts.total}</code> holdings.
      </p>
    </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");

// `desk` and `sector` are declared `enum` without `options`, so both the
// header funnel and the panel's row get a checklist derived from the rows
// themselves — the same distinct-value request, from the same row model.
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),
  },
  {
    id: "price",
    header: "Price",
    type: "number",
    widthPx: 90,
    format: ({ value }) => usd.format(value as number),
  },
  {
    id: "marketValue",
    header: "Market value",
    type: "number",
    widthPx: 120,
    format: ({ value }) => usd.format(value as number),
  },
];
```

```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,
  },
];
```
