Skip to content

Field Panel

The Pivot field panel renders the active field layout above the grid. Use it when users need to see or rearrange row, column, data, and filter fields without opening a separate configurator panel.

Source code
TypeScriptts
import { defineCustomElements } from '@revolist/revogrid/loader';
import { filterPivotDimensions } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';
import {
  PIVOT_FIELDS_PANEL_CHARTS_UI,
  PIVOT_FIELDS_PANEL_DIMENSIONS,
  PIVOT_FIELDS_PANEL_PLUGINS,
  PIVOT_FIELDS_PANEL_ROLES,
  PIVOT_FIELDS_PANEL_ROWS,
  createPivotFieldsPanelCharts,
  createPivotFieldsPanelColumnTypes,
  createPivotFieldsPanelConfig,
  openPivotFieldsPanelChart,
} from './pivotFieldsPanel.shared';

defineCustomElements();

function renderFieldList(
  target: HTMLElement,
  search: string,
  showHidden: boolean,
) {
  const dimensions = filterPivotDimensions(
    PIVOT_FIELDS_PANEL_DIMENSIONS,
    search,
    { showHidden },
  );
  target.replaceChildren(
    ...dimensions.map((dimension) => {
      const item = document.createElement('li');
      const heading = document.createElement('div');
      const label = document.createElement('strong');
      const prop = document.createElement('code');
      const role = document.createElement('span');
      const description = document.createElement('p');
      item.className = 'rounded border border-slate-200 p-3 text-xs';
      heading.className = 'flex flex-wrap items-center gap-2';
      label.textContent = dimension.name ?? String(dimension.prop);
      prop.className = 'rounded bg-slate-100 px-1.5 py-0.5';
      prop.textContent = String(dimension.prop);
      role.className = 'rounded bg-blue-50 px-1.5 py-0.5 text-blue-700';
      role.textContent = PIVOT_FIELDS_PANEL_ROLES[String(dimension.prop)];
      description.className = 'm-0 mt-2 leading-5 text-slate-500';
      description.textContent = dimension.description ?? '';
      heading.append(label, prop, role);

      if (dimension.hidden) {
        const hiddenBadge = document.createElement('span');
        hiddenBadge.className = 'rounded bg-amber-100 px-1.5 py-0.5 text-amber-800';
        hiddenBadge.textContent = 'hidden';
        heading.append(hiddenBadge);
      }

      item.append(heading, description);
      return item;
    }),
  );
}

export function load(
  parentSelector: string,
  rows: any[] = PIVOT_FIELDS_PANEL_ROWS,
) {
  const parent = document.querySelector(parentSelector);
  if (!parent) {
    return;
  }

  const { isDark } = currentTheme();
  const data = Array.isArray(rows) && rows.length > 0
    ? rows
    : PIVOT_FIELDS_PANEL_ROWS;
  const wrapper = document.createElement('div');
  wrapper.className = 'pivot-fields-panel-demo flex h-full min-h-[620px] w-full flex-col gap-3';

  const toolbar = document.createElement('div');
  toolbar.className = 'flex flex-wrap items-center justify-between gap-3 rounded border border-slate-200 bg-white p-3 text-slate-900';
  toolbar.innerHTML = `
    <div>
      <h2 class="m-0 text-sm font-semibold">Pivot Fields Panel</h2>
      <p class="m-0 mt-1 text-xs text-slate-500">Use the fields panel to arrange Rows, Columns, Values, and Filters with readable labels and formatting.</p>
    </div>
    <div class="flex flex-wrap items-center gap-2">
      <button class="open-field-metadata rounded border border-slate-300 bg-white px-3 py-2 text-sm" type="button">Open fields panel</button>
      <button class="create-pivot-chart rounded bg-blue-600 px-3 py-2 text-sm text-white" type="button">Create Pivot Chart</button>
    </div>
  `;

  const metadataDialog = document.createElement('dialog');
  metadataDialog.className = 'm-auto max-h-[90vh] w-[min(1100px,calc(100%-2rem))] rounded-xl border border-slate-200 bg-white p-0 text-slate-900 shadow-2xl backdrop:bg-slate-950/50';
  metadataDialog.setAttribute('aria-labelledby', 'pivot-fields-panel-title');
  metadataDialog.innerHTML = `
    <div class="flex max-h-[90vh] flex-col">
      <header class="flex items-start justify-between gap-4 border-b border-slate-200 px-5 py-4">
        <div>
          <h2 id="pivot-fields-panel-title" class="m-0 text-lg font-semibold">Pivot Fields Panel</h2>
          <p class="m-0 mt-1 max-w-3xl text-sm text-slate-500">Arrange fields across Rows, Columns, Values, and Filters. Field metadata supplies the labels, descriptions, visibility, and formatting shown in the panel.</p>
        </div>
        <button class="close-field-metadata rounded px-2 py-1 text-xl text-slate-500 hover:bg-slate-100" type="button" aria-label="Close field metadata">×</button>
      </header>
      <div class="grid min-h-0 grow gap-4 overflow-auto p-5 lg:grid-cols-[minmax(300px,0.8fr)_minmax(520px,1.2fr)]">
        <section class="flex min-h-0 flex-col gap-3" aria-labelledby="metadata-definitions-title">
          <div>
            <h3 id="metadata-definitions-title" class="m-0 text-sm font-semibold">Field details</h3>
            <p class="m-0 mt-1 text-xs leading-5 text-slate-500"><code>name</code> and <code>description</code> explain fields, <code>hidden</code> controls discovery, and <code>columnType</code> supplies measure formatting. Hidden fields are not removed from the data.</p>
          </div>
          <div class="grid gap-2 sm:grid-cols-3 lg:grid-cols-1">
            <div class="rounded border border-slate-200 bg-slate-50 p-3"><strong class="text-xs">Display</strong><p class="m-0 mt-1 text-xs text-slate-500">Readable labels, descriptions, and currency/integer formats.</p></div>
            <div class="rounded border border-slate-200 bg-slate-50 p-3"><strong class="text-xs">Discovery</strong><p class="m-0 mt-1 text-xs text-slate-500">Internal Segment is hidden by default but still powers a column and filter.</p></div>
            <div class="rounded border border-slate-200 bg-slate-50 p-3"><strong class="text-xs">Pivot rules</strong><p class="m-0 mt-1 text-xs text-slate-500">Empty labels, null labels, totals, and selective subtotal rules are configured separately.</p></div>
          </div>
          <input class="pivot-field-search rounded border border-slate-300 px-3 py-2 text-sm" type="search" placeholder="Search metadata" aria-label="Search field metadata" />
          <label class="flex items-center gap-2 text-xs"><input type="checkbox" />Include hidden fields</label>
          <ul class="m-0 grid list-none gap-2 overflow-auto p-0"></ul>
        </section>
        <section class="flex min-h-[520px] flex-col rounded border border-slate-200" aria-labelledby="pivot-panels-title">
          <div class="border-b border-slate-200 px-4 py-3">
            <h3 id="pivot-panels-title" class="m-0 text-sm font-semibold">Arrange Pivot fields</h3>
            <p class="m-0 mt-1 text-xs text-slate-500">Drag fields between Rows, Columns, Values, and Filters. Changes apply directly to the grid behind this dialog.</p>
          </div>
          <div class="pivot-fields-panel-config min-h-0 grow overflow-auto"></div>
        </section>
      </div>
      <footer class="flex justify-end border-t border-slate-200 px-5 py-3">
        <button class="done-field-metadata rounded bg-slate-900 px-4 py-2 text-sm text-white" type="button">Done</button>
      </footer>
    </div>
  `;

  const searchInput = metadataDialog.querySelector('input[type="search"]') as HTMLInputElement;
  const showHiddenInput = metadataDialog.querySelector('input[type="checkbox"]') as HTMLInputElement;
  const fieldList = metadataDialog.querySelector('ul') as HTMLUListElement;
  const configMount = metadataDialog.querySelector('.pivot-fields-panel-config') as HTMLElement;
  const openMetadataButton = toolbar.querySelector('.open-field-metadata') as HTMLButtonElement;
  const createChartButton = toolbar.querySelector('.create-pivot-chart') as HTMLButtonElement;
  const closeMetadataButtons = metadataDialog.querySelectorAll<HTMLButtonElement>(
    '.close-field-metadata, .done-field-metadata',
  );
  const refreshList = () => renderFieldList(
    fieldList,
    searchInput.value,
    showHiddenInput.checked,
  );
  searchInput.addEventListener('input', refreshList);
  showHiddenInput.addEventListener('change', refreshList);
  refreshList();

  const gridHost = document.createElement('div');
  gridHost.className = 'flex min-h-0 grow flex-col';
  const grid = document.createElement('revo-grid');
  grid.className = 'grow h-full w-full cell-border';
  grid.range = true;
  grid.resize = true;
  grid.filter = true;
  grid.colSize = 170;
  grid.readonly = true;
  grid.hideAttribution = true;
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.columnTypes = createPivotFieldsPanelColumnTypes();
  grid.plugins = PIVOT_FIELDS_PANEL_PLUGINS;
  grid.pivot = createPivotFieldsPanelConfig(configMount);
  grid.pivotCharts = createPivotFieldsPanelCharts();
  grid.pivotChartsUi = PIVOT_FIELDS_PANEL_CHARTS_UI;

  openMetadataButton.addEventListener('click', () => {
    metadataDialog.showModal();
  });
  closeMetadataButtons.forEach((button) => {
    button.addEventListener('click', () => metadataDialog.close());
  });
  metadataDialog.addEventListener('click', (event) => {
    if (event.target === metadataDialog) {
      metadataDialog.close();
    }
  });
  createChartButton.addEventListener('click', () => {
    void openPivotFieldsPanelChart(grid);
  });
  gridHost.append(grid);
  wrapper.append(toolbar, gridHost, metadataDialog);
  parent.append(wrapper);
  grid.source = data;

  return () => {
    grid.remove();
    wrapper.remove();
  };
}
Vuevue
<template>
  <div ref="demoRoot" class="pivot-fields-panel-demo flex h-full min-h-[620px] w-full flex-col gap-3">
    <div class="flex flex-wrap items-center justify-between gap-3 rounded border border-slate-200 bg-white p-3 text-slate-900">
      <div>
        <h2 class="m-0 text-sm font-semibold">Pivot Fields Panel</h2>
        <p class="m-0 mt-1 text-xs text-slate-500">
          Use the fields panel to arrange Rows, Columns, Values, and Filters with readable labels and formatting.
        </p>
      </div>
      <div class="flex flex-wrap items-center gap-2">
        <button
          class="rounded border border-slate-300 bg-white px-3 py-2 text-sm"
          type="button"
          @click="openMetadata"
        >
          Open fields panel
        </button>
        <button
          class="rounded bg-blue-600 px-3 py-2 text-sm text-white"
          type="button"
          @click="openChart"
        >
          Create Pivot Chart
        </button>
      </div>
    </div>
    <div class="flex min-h-0 grow flex-col">
      <RevoGrid
        class="grow h-full cell-border"
        hide-attribution
        range
        resize
        filter
        :colSize="170"
        :source="gridRows"
        :pivot.prop="pivot"
        :pivot-charts.prop="pivotCharts"
        :pivot-charts-ui.prop="pivotChartsUi"
        :additional-data="additionalData"
        :theme="isDark ? 'darkCompact' : 'compact'"
        :plugins="plugins"
        :column-types="columnTypes"
        readonly
      />
    </div>
    <dialog
      ref="metadataDialog"
      aria-labelledby="pivot-fields-panel-title"
      class="m-auto max-h-[90vh] w-[min(1100px,calc(100%-2rem))] rounded-xl border border-slate-200 bg-white p-0 text-slate-900 shadow-2xl backdrop:bg-slate-950/50"
      @click="closeMetadataOnBackdrop"
    >
      <div class="flex max-h-[90vh] flex-col">
        <header class="flex items-start justify-between gap-4 border-b border-slate-200 px-5 py-4">
          <div>
            <h2 id="pivot-fields-panel-title" class="m-0 text-lg font-semibold">
              Pivot Fields Panel
            </h2>
            <p class="m-0 mt-1 max-w-3xl text-sm text-slate-500">
              Arrange fields across Rows, Columns, Values, and Filters. Field metadata supplies the labels, descriptions, visibility, and formatting shown in the panel.
            </p>
          </div>
          <button
            aria-label="Close field metadata"
            class="rounded px-2 py-1 text-xl text-slate-500 hover:bg-slate-100"
            type="button"
            @click="closeMetadata"
          >
            ×
          </button>
        </header>
        <div class="grid min-h-0 grow gap-4 overflow-auto p-5 lg:grid-cols-[minmax(300px,0.8fr)_minmax(520px,1.2fr)]">
          <section class="flex min-h-0 flex-col gap-3" aria-labelledby="metadata-definitions-title">
            <div>
              <h3 id="metadata-definitions-title" class="m-0 text-sm font-semibold">
                Field details
              </h3>
              <p class="m-0 mt-1 text-xs leading-5 text-slate-500">
                <code>name</code> and <code>description</code> explain fields,
                <code>hidden</code> controls discovery, and <code>columnType</code>
                supplies measure formatting. Hidden fields are not removed from the data.
              </p>
            </div>
            <div class="grid gap-2 sm:grid-cols-3 lg:grid-cols-1">
              <div class="rounded border border-slate-200 bg-slate-50 p-3">
                <strong class="text-xs">Display</strong>
                <p class="m-0 mt-1 text-xs text-slate-500">Readable labels, descriptions, and currency/integer formats.</p>
              </div>
              <div class="rounded border border-slate-200 bg-slate-50 p-3">
                <strong class="text-xs">Discovery</strong>
                <p class="m-0 mt-1 text-xs text-slate-500">Internal Segment is hidden by default but still powers a column and filter.</p>
              </div>
              <div class="rounded border border-slate-200 bg-slate-50 p-3">
                <strong class="text-xs">Pivot rules</strong>
                <p class="m-0 mt-1 text-xs text-slate-500">Empty labels, null labels, totals, and selective subtotal rules are configured separately.</p>
              </div>
            </div>
            <input
              v-model="search"
              class="pivot-field-search rounded border border-slate-300 px-3 py-2 text-sm"
              type="search"
              placeholder="Search metadata"
              aria-label="Search field metadata"
            />
            <label class="flex items-center gap-2 text-xs">
              <input v-model="showHidden" type="checkbox" />
              Include hidden fields
            </label>
            <ul class="m-0 grid list-none gap-2 overflow-auto p-0">
              <li
                v-for="dimension in filteredDimensions"
                :key="String(dimension.prop)"
                class="rounded border border-slate-200 p-3 text-xs"
              >
                <div class="flex flex-wrap items-center gap-2">
                  <strong>{{ dimension.name ?? dimension.prop }}</strong>
                  <code class="rounded bg-slate-100 px-1.5 py-0.5">{{ dimension.prop }}</code>
                  <span class="rounded bg-blue-50 px-1.5 py-0.5 text-blue-700">
                    {{ PIVOT_FIELDS_PANEL_ROLES[String(dimension.prop)] }}
                  </span>
                  <span v-if="dimension.hidden" class="rounded bg-amber-100 px-1.5 py-0.5 text-amber-800">
                    hidden
                  </span>
                </div>
                <p class="m-0 mt-2 leading-5 text-slate-500">{{ dimension.description }}</p>
              </li>
            </ul>
          </section>
          <section class="flex min-h-[520px] flex-col rounded border border-slate-200" aria-labelledby="pivot-panels-title">
            <div class="border-b border-slate-200 px-4 py-3">
              <h3 id="pivot-panels-title" class="m-0 text-sm font-semibold">Arrange Pivot fields</h3>
              <p class="m-0 mt-1 text-xs text-slate-500">
                Drag fields between Rows, Columns, Values, and Filters. Changes apply directly to the grid behind this dialog.
              </p>
            </div>
            <div ref="configMount" class="pivot-fields-panel-config min-h-0 grow overflow-auto"></div>
          </section>
        </div>
        <footer class="flex justify-end border-t border-slate-200 px-5 py-3">
          <button class="rounded bg-slate-900 px-4 py-2 text-sm text-white" type="button" @click="closeMetadata">
            Done
          </button>
        </footer>
      </div>
    </dialog>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import RevoGrid, { type GridPlugin } from '@revolist/vue3-datagrid';
import {
  filterPivotDimensions,
} from '@revolist/revogrid-enterprise';
import { currentThemeVue } from '../composables/useRandomData';
import {
  PIVOT_FIELDS_PANEL_CHARTS_UI,
  PIVOT_FIELDS_PANEL_DIMENSIONS,
  PIVOT_FIELDS_PANEL_PLUGINS,
  PIVOT_FIELDS_PANEL_ROLES,
  PIVOT_FIELDS_PANEL_ROWS,
  createPivotFieldsPanelCharts,
  createPivotFieldsPanelColumnTypes,
  createPivotFieldsPanelConfig,
  openPivotFieldsPanelChart,
} from './pivotFieldsPanel.shared';

const props = defineProps<{
  rows?: any[];
}>();

const { isDark } = currentThemeVue();
const search = ref('');
const showHidden = ref(false);
const demoRoot = ref<HTMLElement>();
const metadataDialog = ref<HTMLDialogElement>();
const configMount = ref<HTMLElement>();
const gridRows = ref(
  Array.isArray(props.rows) && props.rows.length > 0
    ? props.rows
    : PIVOT_FIELDS_PANEL_ROWS,
);
const filteredDimensions = computed(() =>
  filterPivotDimensions(PIVOT_FIELDS_PANEL_DIMENSIONS, search.value, { showHidden: showHidden.value }),
);

const columnTypes = ref(createPivotFieldsPanelColumnTypes());
const plugins: GridPlugin[] = PIVOT_FIELDS_PANEL_PLUGINS;
const pivot = computed(() => createPivotFieldsPanelConfig(configMount.value));
const pivotCharts = createPivotFieldsPanelCharts();
const pivotChartsUi = PIVOT_FIELDS_PANEL_CHARTS_UI;
const additionalData = computed(() => ({}));
const openChart = () => openPivotFieldsPanelChart(
  demoRoot.value?.querySelector<HTMLRevoGridElement>('revo-grid'),
);
const openMetadata = () => metadataDialog.value?.showModal();
const closeMetadata = () => metadataDialog.value?.close();
const closeMetadataOnBackdrop = (event: MouseEvent) => {
  if (event.target === metadataDialog.value) {
    closeMetadata();
  }
};
</script>
Reacttsx
import { useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { filterPivotDimensions } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';
import {
  PIVOT_FIELDS_PANEL_CHARTS_UI,
  PIVOT_FIELDS_PANEL_DIMENSIONS,
  PIVOT_FIELDS_PANEL_PLUGINS,
  PIVOT_FIELDS_PANEL_ROLES,
  PIVOT_FIELDS_PANEL_ROWS,
  createPivotFieldsPanelCharts,
  createPivotFieldsPanelColumnTypes,
  createPivotFieldsPanelConfig,
  openPivotFieldsPanelChart,
} from './pivotFieldsPanel.shared';

interface PivotFieldsPanelProps {
  rows?: any[];
}

function PivotFieldsPanel({ rows }: PivotFieldsPanelProps) {
  const { isDark } = currentTheme();
  const [search, setSearch] = useState('');
  const [showHidden, setShowHidden] = useState(false);
  const [configMount, setConfigMount] = useState<HTMLElement | null>(null);
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const metadataDialogRef = useRef<HTMLDialogElement>(null);

  const data = useMemo(
    () => Array.isArray(rows) && rows.length > 0
      ? rows
      : PIVOT_FIELDS_PANEL_ROWS,
    [rows],
  );
  const filteredDimensions = useMemo(
    () => filterPivotDimensions(
      PIVOT_FIELDS_PANEL_DIMENSIONS,
      search,
      { showHidden },
    ),
    [search, showHidden],
  );
  const columnTypes = useMemo(createPivotFieldsPanelColumnTypes, []);
  const plugins = useMemo(() => PIVOT_FIELDS_PANEL_PLUGINS, []);
  const additionalData = useMemo(() => ({}), []);
  const pivot = useMemo(
    () => createPivotFieldsPanelConfig(configMount ?? undefined),
    [configMount],
  );
  const pivotCharts = useMemo(createPivotFieldsPanelCharts, []);
  const pivotChartsUi = useMemo(
    () => PIVOT_FIELDS_PANEL_CHARTS_UI,
    [],
  );

  return (
    <div className="pivot-fields-panel-demo flex h-full min-h-[620px] w-full flex-col gap-3">
      <div className="flex flex-wrap items-center justify-between gap-3 rounded border border-slate-200 bg-white p-3 text-slate-900">
        <div>
          <h2 className="m-0 text-sm font-semibold">Pivot Fields Panel</h2>
          <p className="m-0 mt-1 text-xs text-slate-500">
            Metadata gives raw fields readable names, descriptions, visibility,
            formatting, and Pivot roles.
          </p>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          <button
            className="rounded border border-slate-300 bg-white px-3 py-2 text-sm"
            type="button"
            onClick={() => metadataDialogRef.current?.showModal()}
          >
            Open fields panel
          </button>
          <button
            className="rounded bg-blue-600 px-3 py-2 text-sm text-white"
            type="button"
            onClick={() => void openPivotFieldsPanelChart(gridRef.current)}
          >
            Create Pivot Chart
          </button>
        </div>
      </div>
      <div className="flex min-h-0 grow flex-col">
        <RevoGrid
          ref={gridRef}
          className="grow h-full cell-border"
          hideAttribution
          range
          resize
          filter
          colSize={170}
          source={data}
          pivot={pivot}
          pivotCharts={pivotCharts}
          pivotChartsUi={pivotChartsUi}
          additionalData={additionalData}
          theme={isDark() ? 'darkCompact' : 'compact'}
          plugins={plugins}
          columnTypes={columnTypes}
          readonly
        />
      </div>
      <dialog
        ref={metadataDialogRef}
        aria-labelledby="pivot-fields-panel-title"
        className="m-auto max-h-[90vh] w-[min(1100px,calc(100%-2rem))] rounded-xl border border-slate-200 bg-white p-0 text-slate-900 shadow-2xl backdrop:bg-slate-950/50"
        onClick={(event) => {
          if (event.target === event.currentTarget) {
            event.currentTarget.close();
          }
        }}
      >
        <div className="flex max-h-[90vh] flex-col">
          <header className="flex items-start justify-between gap-4 border-b border-slate-200 px-5 py-4">
            <div>
              <h2
                id="pivot-fields-panel-title"
                className="m-0 text-lg font-semibold"
              >
                Pivot Fields Panel
              </h2>
              <p className="m-0 mt-1 max-w-3xl text-sm text-slate-500">
                The source rows keep their original property keys. Metadata
                adds the labels and behavior that users see when they build a
                Pivot.
              </p>
            </div>
            <button
              aria-label="Close field metadata"
              className="rounded px-2 py-1 text-xl text-slate-500 hover:bg-slate-100"
              type="button"
              onClick={() => metadataDialogRef.current?.close()}
            >
              ×
            </button>
          </header>
          <div className="grid min-h-0 grow gap-4 overflow-auto p-5 lg:grid-cols-[minmax(300px,0.8fr)_minmax(520px,1.2fr)]">
            <section
              className="flex min-h-0 flex-col gap-3"
              aria-labelledby="metadata-definitions-title"
            >
              <div>
                <h3
                  id="metadata-definitions-title"
                  className="m-0 text-sm font-semibold"
                >
                  Field details
                </h3>
                <p className="m-0 mt-1 text-xs leading-5 text-slate-500">
                  <code>name</code> and <code>description</code> explain
                  fields, <code>hidden</code> controls discovery, and{' '}
                  <code>columnType</code> supplies measure formatting. Hidden
                  fields are not removed from the data.
                </p>
              </div>
              <div className="grid gap-2 sm:grid-cols-3 lg:grid-cols-1">
                <div className="rounded border border-slate-200 bg-slate-50 p-3">
                  <strong className="text-xs">Display</strong>
                  <p className="m-0 mt-1 text-xs text-slate-500">
                    Readable labels, descriptions, and currency/integer formats.
                  </p>
                </div>
                <div className="rounded border border-slate-200 bg-slate-50 p-3">
                  <strong className="text-xs">Discovery</strong>
                  <p className="m-0 mt-1 text-xs text-slate-500">
                    Internal Segment is hidden by default but still powers a
                    column and filter.
                  </p>
                </div>
                <div className="rounded border border-slate-200 bg-slate-50 p-3">
                  <strong className="text-xs">Pivot rules</strong>
                  <p className="m-0 mt-1 text-xs text-slate-500">
                    Empty labels, null labels, totals, and selective subtotal
                    rules are configured separately.
                  </p>
                </div>
              </div>
              <input
                className="pivot-field-search rounded border border-slate-300 px-3 py-2 text-sm"
                type="search"
                placeholder="Search metadata"
                aria-label="Search field metadata"
                value={search}
                onChange={(event) => setSearch(event.currentTarget.value)}
              />
              <label className="flex items-center gap-2 text-xs">
                <input
                  type="checkbox"
                  checked={showHidden}
                  onChange={(event) => (
                    setShowHidden(event.currentTarget.checked)
                  )}
                />
                Include hidden fields
              </label>
              <ul className="m-0 grid list-none gap-2 overflow-auto p-0">
                {filteredDimensions.map((dimension) => (
                  <li
                    className="rounded border border-slate-200 p-3 text-xs"
                    key={String(dimension.prop)}
                  >
                    <div className="flex flex-wrap items-center gap-2">
                      <strong>
                        {dimension.name ?? String(dimension.prop)}
                      </strong>
                      <code className="rounded bg-slate-100 px-1.5 py-0.5">
                        {String(dimension.prop)}
                      </code>
                      <span className="rounded bg-blue-50 px-1.5 py-0.5 text-blue-700">
                        {PIVOT_FIELDS_PANEL_ROLES[String(dimension.prop)]}
                      </span>
                      {dimension.hidden && (
                        <span className="rounded bg-amber-100 px-1.5 py-0.5 text-amber-800">
                          hidden
                        </span>
                      )}
                    </div>
                    <p className="m-0 mt-2 leading-5 text-slate-500">
                      {dimension.description}
                    </p>
                  </li>
                ))}
              </ul>
            </section>
            <section
              className="flex min-h-[520px] flex-col rounded border border-slate-200"
              aria-labelledby="pivot-panels-title"
            >
              <div className="border-b border-slate-200 px-4 py-3">
                <h3
                  id="pivot-panels-title"
                  className="m-0 text-sm font-semibold"
                >
                  Arrange Pivot fields
                </h3>
                <p className="m-0 mt-1 text-xs text-slate-500">
                  Drag fields between Rows, Columns, Values, and Filters.
                  Changes apply directly to the grid behind this dialog.
                </p>
              </div>
              <div
                ref={setConfigMount}
                className="pivot-fields-panel-config min-h-0 grow overflow-auto"
              />
            </section>
          </div>
          <footer className="flex justify-end border-t border-slate-200 px-5 py-3">
            <button
              className="rounded bg-slate-900 px-4 py-2 text-sm text-white"
              type="button"
              onClick={() => metadataDialogRef.current?.close()}
            >
              Done
            </button>
          </footer>
        </div>
      </dialog>
    </div>
  );
}

export default PivotFieldsPanel;
Angularts
import { CommonModule } from '@angular/common';
import {
  Component,
  ElementRef,
  Input,
  NO_ERRORS_SCHEMA,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { filterPivotDimensions } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';
import {
  PIVOT_FIELDS_PANEL_CHARTS_UI,
  PIVOT_FIELDS_PANEL_DIMENSIONS,
  PIVOT_FIELDS_PANEL_PLUGINS,
  PIVOT_FIELDS_PANEL_ROLES,
  PIVOT_FIELDS_PANEL_ROWS,
  createPivotFieldsPanelCharts,
  createPivotFieldsPanelColumnTypes,
  createPivotFieldsPanelConfig,
  openPivotFieldsPanelChart,
} from './pivotFieldsPanel.shared';

@Component({
  selector: 'pivot-fields-panel-grid',
  standalone: true,
  imports: [CommonModule, RevoGrid],
  schemas: [NO_ERRORS_SCHEMA],
  template: `
    <div class="pivot-fields-panel-demo flex h-full min-h-[620px] w-full flex-col gap-3">
      <div class="flex flex-wrap items-center justify-between gap-3 rounded border border-slate-200 bg-white p-3 text-slate-900">
        <div>
          <h2 class="m-0 text-sm font-semibold">Pivot Fields Panel</h2>
          <p class="m-0 mt-1 text-xs text-slate-500">
            Use the fields panel to arrange Rows, Columns, Values, and Filters with readable labels and formatting.
          </p>
        </div>
        <div class="flex flex-wrap items-center gap-2">
          <button
            class="rounded border border-slate-300 bg-white px-3 py-2 text-sm"
            type="button"
            (click)="openMetadata()"
          >
            Open fields panel
          </button>
          <button
            class="rounded bg-blue-600 px-3 py-2 text-sm text-white"
            type="button"
            (click)="openChart()"
          >
            Create Pivot Chart
          </button>
        </div>
      </div>
      <div class="flex min-h-0 grow flex-col">
        <revo-grid
          #grid
          class="grow h-full cell-border"
          [hideAttribution]="true"
          [range]="true"
          [resize]="true"
          [filter]="true"
          [colSize]="170"
          [source]="gridRows"
          [pivot]="pivot"
          [pivotCharts]="pivotCharts"
          [pivotChartsUi]="pivotChartsUi"
          [additionalData]="additionalData"
          [theme]="theme"
          [plugins]="plugins"
          [columnTypes]="columnTypes"
          [readonly]="true"
        ></revo-grid>
      </div>
      <dialog
        #metadataDialog
        aria-labelledby="pivot-fields-panel-title"
        class="m-auto max-h-[90vh] w-[min(1100px,calc(100%-2rem))] rounded-xl border border-slate-200 bg-white p-0 text-slate-900 shadow-2xl backdrop:bg-slate-950/50"
        (click)="closeMetadataOnBackdrop($event)"
      >
        <div class="flex max-h-[90vh] flex-col">
          <header class="flex items-start justify-between gap-4 border-b border-slate-200 px-5 py-4">
            <div>
              <h2 id="pivot-fields-panel-title" class="m-0 text-lg font-semibold">
                Pivot Fields Panel
              </h2>
              <p class="m-0 mt-1 max-w-3xl text-sm text-slate-500">
                Arrange fields across Rows, Columns, Values, and Filters. Field metadata supplies the labels, descriptions, visibility, and formatting shown in the panel.
              </p>
            </div>
            <button
              aria-label="Close field metadata"
              class="rounded px-2 py-1 text-xl text-slate-500 hover:bg-slate-100"
              type="button"
              (click)="closeMetadata()"
            >×</button>
          </header>
          <div class="grid min-h-0 grow gap-4 overflow-auto p-5 lg:grid-cols-[minmax(300px,0.8fr)_minmax(520px,1.2fr)]">
            <section class="flex min-h-0 flex-col gap-3" aria-labelledby="metadata-definitions-title">
              <div>
                <h3 id="metadata-definitions-title" class="m-0 text-sm font-semibold">
                  Field details
                </h3>
                <p class="m-0 mt-1 text-xs leading-5 text-slate-500">
                  <code>name</code> and <code>description</code> explain fields,
                  <code>hidden</code> controls discovery, and <code>columnType</code>
                  supplies measure formatting. Hidden fields are not removed from the data.
                </p>
              </div>
              <div class="grid gap-2 sm:grid-cols-3 lg:grid-cols-1">
                <div class="rounded border border-slate-200 bg-slate-50 p-3">
                  <strong class="text-xs">Display</strong>
                  <p class="m-0 mt-1 text-xs text-slate-500">Readable labels, descriptions, and currency/integer formats.</p>
                </div>
                <div class="rounded border border-slate-200 bg-slate-50 p-3">
                  <strong class="text-xs">Discovery</strong>
                  <p class="m-0 mt-1 text-xs text-slate-500">Internal Segment is hidden by default but still powers a column and filter.</p>
                </div>
                <div class="rounded border border-slate-200 bg-slate-50 p-3">
                  <strong class="text-xs">Pivot rules</strong>
                  <p class="m-0 mt-1 text-xs text-slate-500">Empty labels, null labels, totals, and selective subtotal rules are configured separately.</p>
                </div>
              </div>
              <input
                class="pivot-field-search rounded border border-slate-300 px-3 py-2 text-sm"
                type="search"
                placeholder="Search metadata"
                aria-label="Search field metadata"
                [value]="search"
                (input)="setSearch($event)"
              />
              <label class="flex items-center gap-2 text-xs">
                <input
                  type="checkbox"
                  [checked]="showHidden"
                  (change)="setShowHidden($event)"
                />
                Include hidden fields
              </label>
              <ul class="m-0 grid list-none gap-2 overflow-auto p-0">
                <li
                  *ngFor="let dimension of filteredDimensions"
                  class="rounded border border-slate-200 p-3 text-xs"
                >
                  <div class="flex flex-wrap items-center gap-2">
                    <strong>{{ dimension.name || dimension.prop }}</strong>
                    <code class="rounded bg-slate-100 px-1.5 py-0.5">{{ dimension.prop }}</code>
                    <span class="rounded bg-blue-50 px-1.5 py-0.5 text-blue-700">
                      {{ fieldRoles[dimension.prop] }}
                    </span>
                    <span
                      *ngIf="dimension.hidden"
                      class="rounded bg-amber-100 px-1.5 py-0.5 text-amber-800"
                    >hidden</span>
                  </div>
                  <p class="m-0 mt-2 leading-5 text-slate-500">{{ dimension.description }}</p>
                </li>
              </ul>
            </section>
            <section class="flex min-h-[520px] flex-col rounded border border-slate-200" aria-labelledby="pivot-panels-title">
              <div class="border-b border-slate-200 px-4 py-3">
                <h3 id="pivot-panels-title" class="m-0 text-sm font-semibold">Arrange Pivot fields</h3>
                <p class="m-0 mt-1 text-xs text-slate-500">
                  Drag fields between Rows, Columns, Values, and Filters. Changes apply directly to the grid behind this dialog.
                </p>
              </div>
              <div #configMount class="pivot-fields-panel-config min-h-0 grow overflow-auto"></div>
            </section>
          </div>
          <footer class="flex justify-end border-t border-slate-200 px-5 py-3">
            <button class="rounded bg-slate-900 px-4 py-2 text-sm text-white" type="button" (click)="closeMetadata()">
              Done
            </button>
          </footer>
        </div>
      </dialog>
    </div>
  `,
  encapsulation: ViewEncapsulation.None,
})
export class PivotFieldsPanelGridComponent {
  @Input() rows?: any[];
  @ViewChild('grid', { read: ElementRef })
  gridRef?: ElementRef<HTMLRevoGridElement>;
  @ViewChild('metadataDialog', { read: ElementRef })
  metadataDialogRef?: ElementRef<HTMLDialogElement>;
  @ViewChild('configMount', { read: ElementRef })
  set configMountRef(ref: ElementRef<HTMLElement> | undefined) {
    if (ref) {
      this.pivot = createPivotFieldsPanelConfig(ref.nativeElement);
    }
  }

  search = '';
  showHidden = false;
  fieldRoles = PIVOT_FIELDS_PANEL_ROLES;
  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  columnTypes = createPivotFieldsPanelColumnTypes();
  plugins = PIVOT_FIELDS_PANEL_PLUGINS;
  pivot = createPivotFieldsPanelConfig();
  pivotCharts = createPivotFieldsPanelCharts();
  pivotChartsUi = PIVOT_FIELDS_PANEL_CHARTS_UI;
  additionalData = {};

  get gridRows() {
    return Array.isArray(this.rows) && this.rows.length > 0
      ? this.rows
      : PIVOT_FIELDS_PANEL_ROWS;
  }

  get filteredDimensions() {
    return filterPivotDimensions(
      PIVOT_FIELDS_PANEL_DIMENSIONS,
      this.search,
      { showHidden: this.showHidden },
    );
  }

  setSearch(event: Event) {
    this.search = (event.target as HTMLInputElement).value;
  }

  setShowHidden(event: Event) {
    this.showHidden = (event.target as HTMLInputElement).checked;
  }

  openChart() {
    return openPivotFieldsPanelChart(this.gridRef?.nativeElement);
  }

  openMetadata() {
    this.metadataDialogRef?.nativeElement.showModal();
  }

  closeMetadata() {
    this.metadataDialogRef?.nativeElement.close();
  }

  closeMetadataOnBackdrop(event: MouseEvent) {
    if (event.target === this.metadataDialogRef?.nativeElement) {
      this.closeMetadata();
    }
  }
}
const pivot = {
dimensions: [...],
rows: ['region', 'rep'],
columns: ['year', 'quarter'],
values: [{ prop: 'sales', aggregator: 'sum' }],
filters: ['year'],
fieldPanel: {
visible: true,
showResetLayout: false,
allowFieldDragging: true,
allowFieldRemoving: false,
showDataFields: true,
showRowFields: true,
showColumnFields: true,
showFilterFields: true,
texts: {
dropFilterFieldsHere: 'Drop Filter Fields Here',
},
},
};
  • fieldPanel.visible: mounts the compact in-grid field panel.
  • fieldPanel.showResetLayout: shows the Reset layout action in Pivot configuration UI. Default is false.
  • allowFieldDragging: enables drag-and-drop between panel areas.
  • allowFieldRemoving: enables explicit removal from areas. Default is false.
  • showDataFields, showRowFields, showColumnFields, showFilterFields: toggle individual areas.
  • fieldPanel.texts: overrides field panel labels and placeholders.
  • filters: stores the ordered filter fields shown in the filter area.

fieldPanel.visible is separate from hasConfigurator. Use fieldPanel.visible for the compact in-grid field strip, and hasConfigurator for the full side configurator.