Skip to content

Field Metadata

Field metadata is the information Pivot uses to describe fields before they become row groups, column groups, filters, or measures. Use it to give users clearer labels, keep internal fields out of normal field lists, and make generated totals easier to read.

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();
    }
  }
}

Mark a dimension as hidden when it should still be available to the Pivot config but should not appear in normal field lists.

const dimensions = [
{ prop: 'region', name: 'Region' },
{ prop: 'quarter', name: 'Quarter' },
{
prop: 'internalSegment',
name: 'Internal Segment',
description: 'Internal reporting segment.',
hidden: true,
},
];
const pivot = {
dimensions,
rows: ['region'],
columns: ['quarter', 'internalSegment'],
filters: ['internalSegment'],
values: [{ prop: 'revenue', aggregator: 'sum' }],
};

Hidden does not remove the field from the Pivot engine. In this example, internalSegment can still be used in columns and filters; it is only hidden from helper-driven field lists unless you explicitly show hidden fields.

Use filterPivotDimensions for searchable field lists. Hidden dimensions are omitted by default. Pass { showHidden: true } when you want an admin or advanced-user mode that reveals them.

import { filterPivotDimensions } from '@revolist/revogrid-enterprise';
const visibleFields = filterPivotDimensions(dimensions, searchText);
const allFields = filterPivotDimensions(dimensions, searchText, {
showHidden: true,
});

The helper searches prop, name, and description, so descriptions are useful for discovery even when the visible label is short.

Real data often contains empty strings, null, or undefined. Pivot can replace those values with user-facing labels in row and column groups.

const pivot = {
rows: ['region', 'channel'],
columns: ['quarter'],
values: [{ prop: 'revenue', aggregator: 'sum' }],
groupLabels: {
empty: '(Empty)',
null: '(No value)',
},
};

groupLabels.empty is used for empty strings (''). groupLabels.null is used for null and undefined. Without these options, the current implementation displays empty labels for those group members.

Each value can have its own display label. Use label for the preferred generated measure name. Use name as a fallback when label is not set.

const pivot = {
values: [
{ prop: 'revenue', aggregator: 'sum', label: 'Revenue $' },
{ prop: 'units', aggregator: 'sum', name: 'Units Sold' },
],
};

Aliases are useful when the source field name is technical, abbreviated, or shared by multiple reports.

Global subtotals can stay enabled while specific subtotal rows or subtotal columns are skipped. Target subtotals by axis field or by zero-based axis level.

const pivot = {
rows: ['region', 'channel', 'product'],
columns: ['quarter', 'internalSegment'],
values: [{ prop: 'revenue', aggregator: 'sum' }],
totals: {
grandTotal: true,
subtotals: true,
disabledSubtotals: {
rows: {
fields: ['channel'],
},
columns: {
levels: [0],
},
},
},
};

In this example, Pivot keeps subtotal generation enabled overall, skips row subtotals for the channel field, and skips column subtotals at level 0.

const pivot = {
dimensions: [
{ prop: 'region', name: 'Region', description: 'Sales territory.' },
{ prop: 'channel', name: 'Channel', description: 'Go-to-market channel.' },
{ prop: 'product', name: 'Product' },
{ prop: 'quarter', name: 'Quarter' },
{ prop: 'internalSegment', name: 'Internal Segment', hidden: true },
{ prop: 'revenue', name: 'Revenue' },
{ prop: 'units', name: 'Units' },
],
rows: ['region', 'channel', 'product'],
columns: ['quarter', 'internalSegment'],
filters: ['internalSegment'],
values: [
{ prop: 'revenue', aggregator: 'sum', label: 'Revenue $' },
{ prop: 'units', aggregator: 'sum', name: 'Units Sold' },
],
groupLabels: {
empty: '(Empty)',
null: '(No value)',
},
totals: {
grandTotal: true,
subtotals: true,
disabledSubtotals: {
rows: { fields: ['channel'] },
columns: { levels: [0] },
},
},
};
  • Expecting hidden: true to prevent a field from being used in rows, columns, filters, or values. It only affects field-list helpers.
  • Passing { showHidden: true } to the Pivot config. It belongs to filterPivotDimensions, not pivot.
  • Using only groupLabels.empty when the source data contains null or undefined.
  • Disabling totals.subtotals and then expecting disabledSubtotals to do anything. Selective disabling only matters when subtotals: true.