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
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();
};
}
<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>
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;
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();
}
}
}
Hidden Dimensions
Section titled “Hidden Dimensions”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.
Show Hidden Fields In Search
Section titled “Show Hidden Fields In Search”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.
Empty And Null Group Labels
Section titled “Empty And Null Group Labels”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.
Measure Aliases
Section titled “Measure Aliases”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.
Disabled Subtotals
Section titled “Disabled Subtotals”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.
Complete Config
Section titled “Complete Config”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] }, }, },};Common Mistakes
Section titled “Common Mistakes”- Expecting
hidden: trueto prevent a field from being used inrows,columns,filters, orvalues. It only affects field-list helpers. - Passing
{ showHidden: true }to the Pivot config. It belongs tofilterPivotDimensions, notpivot. - Using only
groupLabels.emptywhen the source data containsnullorundefined. - Disabling
totals.subtotalsand then expectingdisabledSubtotalsto do anything. Selective disabling only matters whensubtotals: true.