Values On Rows
By default, Pivot renders measures as generated value columns. valuesOnRows flips that layout so measures become synthetic row members instead.
Source code
TypeScript ts
// src/components/pivot/PivotValuesOnRows.ts
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { currentTheme } from '../composables/useRandomData';
import {
PIVOT_VALUES_ON_ROWS,
PIVOT_VALUES_ON_ROWS_DATA,
PIVOT_VALUES_ON_ROWS_PLUGINS,
} from '../sys-data/pivot.values-on-rows';
type ValuesLayout = 'columns' | 'end' | 'middle' | 'beginning';
const ROW_TREE_BY_LAYOUT = {
end: ['region', 'rep', '$values'],
middle: ['region', '$values', 'rep'],
beginning: ['$values', 'region', 'rep'],
} as const;
export function load(parentSelector: string, rows: any[] | { isDark?: boolean } = PIVOT_VALUES_ON_ROWS_DATA) {
const { isDark } = currentTheme();
// Ensure rows is an array with data, otherwise fallback to default data
const data = Array.isArray(rows) && rows.length > 0 ? rows : PIVOT_VALUES_ON_ROWS_DATA;
const container = document.createElement('div');
container.className = 'grow h-full flex flex-col gap-2';
const controls = document.createElement('div');
controls.className = 'rv-btn-group';
const layoutOptions = [
['columns', 'Values as columns'],
['end', 'Values at end'],
['middle', 'Values in middle'],
['beginning', 'Values at beginning'],
] as const;
let valuesLayout: ValuesLayout = 'end';
layoutOptions.forEach(([value, text]) => {
const label = document.createElement('label');
label.className = 'rv-btn-group-item';
const input = document.createElement('input');
input.type = 'radio';
input.name = 'pivot-values-layout';
input.value = value;
input.checked = value === valuesLayout;
const labelText = document.createElement('span');
labelText.textContent = text;
input.addEventListener('change', () => {
valuesLayout = value;
updateConfig();
});
label.appendChild(input);
label.appendChild(labelText);
controls.appendChild(label);
});
const grid = document.createElement('revo-grid') as any;
grid.className = 'flex-1 min-h-0 cell-border';
// Set all properties at once
Object.assign(grid, {
range: true,
resize: true,
filter: true,
colSize: 180,
readonly: true,
hideAttribution: true,
theme: isDark() ? 'darkCompact' : 'compact',
plugins: PIVOT_VALUES_ON_ROWS_PLUGINS,
columnTypes: {
currency: new NumberColumnType('$0,0.00'),
},
columns: [],
});
const updateConfig = () => {
const layout = valuesLayout;
Object.assign(grid, {
pivot: {
...PIVOT_VALUES_ON_ROWS,
valuesOnRows: layout !== 'columns',
...(layout === 'columns'
? {}
: { rowTree: [...ROW_TREE_BY_LAYOUT[layout]] }),
},
})
};
updateConfig();
container.appendChild(controls);
container.appendChild(grid);
document.querySelector(parentSelector)?.appendChild(container);
// Set source last after DOM attachment and config
grid.source = data;
return () => {
container.remove();
};
}
Vue vue
<template>
<div class="grow h-full flex flex-col gap-2">
<div class="rv-btn-group">
<label v-for="option in valuesLayoutOptions" :key="option.value" class="rv-btn-group-item">
<input v-model="valuesLayout" type="radio" name="pivot-values-layout" :value="option.value" />
<span>{{ option.label }}</span>
</label>
</div>
<RevoGrid
class="flex-1 min-h-0 cell-border"
hide-attribution
range
resize
filter
:colSize="180"
:source="rows"
:pivot.prop="pivot"
:theme="isDark ? 'darkCompact' : 'compact'"
:plugins="plugins"
:column-types="columnTypes"
readonly
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { currentThemeVue } from '../composables/useRandomData';
import {
PIVOT_VALUES_ON_ROWS,
PIVOT_VALUES_ON_ROWS_DATA,
PIVOT_VALUES_ON_ROWS_PLUGINS,
} from '../sys-data/pivot.values-on-rows';
type ValuesLayout = 'columns' | 'end' | 'middle' | 'beginning';
const ROW_TREE_BY_LAYOUT = {
end: ['region', 'rep', '$values'],
middle: ['region', '$values', 'rep'],
beginning: ['$values', 'region', 'rep'],
} as const;
const valuesLayoutOptions: Array<{ value: ValuesLayout; label: string }> = [
{ value: 'columns', label: 'Values as columns' },
{ value: 'end', label: 'Values at end' },
{ value: 'middle', label: 'Values in middle' },
{ value: 'beginning', label: 'Values at beginning' },
];
const { isDark } = currentThemeVue();
const props = defineProps<{
rows: any[];
}>();
const rows = computed(() => props.rows?.length ? props.rows : PIVOT_VALUES_ON_ROWS_DATA);
const plugins = PIVOT_VALUES_ON_ROWS_PLUGINS;
const columnTypes = ref({
currency: new NumberColumnType('$0,0.00'),
});
const valuesLayout = ref<ValuesLayout>('end');
const pivot = computed(() => ({
...PIVOT_VALUES_ON_ROWS,
valuesOnRows: valuesLayout.value !== 'columns',
...(valuesLayout.value === 'columns'
? {}
: { rowTree: [...ROW_TREE_BY_LAYOUT[valuesLayout.value]] }),
}));
</script>
React tsx
// src/components/pivot/PivotValuesOnRows.tsx
import { useMemo, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { currentTheme } from '../composables/useRandomData';
import {
PIVOT_VALUES_ON_ROWS,
PIVOT_VALUES_ON_ROWS_DATA,
PIVOT_VALUES_ON_ROWS_PLUGINS,
} from '../sys-data/pivot.values-on-rows';
interface PivotValuesOnRowsProps {
rows?: any[];
}
type ValuesLayout = 'columns' | 'end' | 'middle' | 'beginning';
const ROW_TREE_BY_LAYOUT = {
end: ['region', 'rep', '$values'],
middle: ['region', '$values', 'rep'],
beginning: ['$values', 'region', 'rep'],
} as const;
const VALUES_LAYOUT_OPTIONS: Array<{ value: ValuesLayout; label: string }> = [
{ value: 'columns', label: 'Values as columns' },
{ value: 'end', label: 'Values at end' },
{ value: 'middle', label: 'Values in middle' },
{ value: 'beginning', label: 'Values at beginning' },
];
function PivotValuesOnRows({ rows }: PivotValuesOnRowsProps) {
const { isDark } = currentTheme();
const data = useMemo(() => (Array.isArray(rows) && rows.length > 0 ? rows : PIVOT_VALUES_ON_ROWS_DATA), [rows]);
const [valuesLayout, setValuesLayout] = useState<ValuesLayout>('end');
const columnTypes = useMemo(
() => ({
currency: new NumberColumnType('$0,0.00'),
}),
[],
);
const pivot = useMemo(
() => ({
...PIVOT_VALUES_ON_ROWS,
valuesOnRows: valuesLayout !== 'columns',
...(valuesLayout === 'columns'
? {}
: { rowTree: [...ROW_TREE_BY_LAYOUT[valuesLayout]] }),
}),
[valuesLayout],
);
return (
<div className="grow h-full flex flex-col gap-2">
<div className="rv-btn-group">
{VALUES_LAYOUT_OPTIONS.map(option => (
<label key={option.value} className="rv-btn-group-item">
<input
type="radio"
name="pivot-values-layout"
value={option.value}
checked={valuesLayout === option.value}
onChange={() => setValuesLayout(option.value)}
/>
<span>{option.label}</span>
</label>
))}
</div>
<RevoGrid
className="flex-1 min-h-0 cell-border"
hideAttribution
range
resize
filter
colSize={180}
source={data}
columns={[]}
pivot={pivot}
theme={isDark() ? 'darkCompact' : 'compact'}
plugins={PIVOT_VALUES_ON_ROWS_PLUGINS}
columnTypes={columnTypes}
readonly
/>
</div>
);
}
export default PivotValuesOnRows;
Angular ts
import { Component, NO_ERRORS_SCHEMA, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RevoGrid } from '@revolist/angular-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { currentTheme } from '../composables/useRandomData';
import {
PIVOT_VALUES_ON_ROWS,
PIVOT_VALUES_ON_ROWS_DATA,
PIVOT_VALUES_ON_ROWS_PLUGINS,
} from '../sys-data/pivot.values-on-rows';
type ValuesLayout = 'columns' | 'end' | 'middle' | 'beginning';
const ROW_TREE_BY_LAYOUT = {
end: ['region', 'rep', '$values'],
middle: ['region', '$values', 'rep'],
beginning: ['$values', 'region', 'rep'],
} as const;
const VALUES_LAYOUT_OPTIONS: Array<{ value: ValuesLayout; label: string }> = [
{ value: 'columns', label: 'Values as columns' },
{ value: 'end', label: 'Values at end' },
{ value: 'middle', label: 'Values in middle' },
{ value: 'beginning', label: 'Values at beginning' },
];
@Component({
selector: 'pivot-values-on-rows-grid',
standalone: true,
imports: [RevoGrid, FormsModule],
// Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
schemas: [NO_ERRORS_SCHEMA],
template: `
<div class="grow h-full flex flex-col gap-2" style="min-height: 640px">
<div class="rv-btn-group">
@for (option of valuesLayoutOptions; track option.value) {
<label class="rv-btn-group-item">
<input
type="radio"
name="pivot-values-layout"
[value]="option.value"
[(ngModel)]="valuesLayout"
(ngModelChange)="updateConfig()"
/>
<span>{{ option.label }}</span>
</label>
}
</div>
<revo-grid
class="flex-1 min-h-0 cell-border"
style="min-height: 560px"
[hideAttribution]="true"
[range]="true"
[resize]="true"
[filter]="true"
[colSize]="180"
[source]="rows"
[pivot]="pivot"
[theme]="theme"
[plugins]="plugins"
[columnTypes]="columnTypes"
[readonly]="true"
></revo-grid>
</div>
`,
encapsulation: ViewEncapsulation.None,
})
export class PivotValuesOnRowsGridComponent {
rows = PIVOT_VALUES_ON_ROWS_DATA;
theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
columnTypes = {
currency: new NumberColumnType('$0,0.00'),
};
plugins = PIVOT_VALUES_ON_ROWS_PLUGINS;
valuesLayoutOptions = VALUES_LAYOUT_OPTIONS;
valuesLayout: ValuesLayout = 'end';
pivot = this.createPivotConfig();
updateConfig() {
this.pivot = this.createPivotConfig();
}
private createPivotConfig() {
return {
...PIVOT_VALUES_ON_ROWS,
valuesOnRows: this.valuesLayout !== 'columns',
...(this.valuesLayout === 'columns'
? {}
: { rowTree: [...ROW_TREE_BY_LAYOUT[this.valuesLayout]] }),
};
}
}
Data & Config ts
import type { GridPlugin } from '@revolist/revogrid';
import { PivotPlugin, type PivotConfig } from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, RowOddPlugin, commonAggregators } from '@revolist/revogrid-pro';
import { PIVOT_TIME_RANGE_ROWS } from './pivot.shared';
export const PIVOT_VALUES_ON_ROWS_DATA = PIVOT_TIME_RANGE_ROWS;
export const PIVOT_VALUES_ON_ROWS_PLUGINS: GridPlugin[] = [
PivotPlugin,
AdvanceFilterPlugin,
RowOddPlugin,
] as any[];
export const PIVOT_VALUES_ON_ROWS: PivotConfig = {
dimensions: [
{ prop: 'region', sortable: true },
{ prop: 'rep', sortable: true },
{ prop: 'year', sortable: true },
{ prop: 'quarter', sortable: true },
{
prop: 'sales',
name: 'Sales',
columnType: 'currency',
aggregators: {
sum: commonAggregators.sum,
avg: commonAggregators.avg,
},
},
],
rows: ['region', 'rep'],
columns: ['year', 'quarter'],
values: [
{ prop: 'sales', aggregator: 'sum' },
{ prop: 'sales', aggregator: 'avg' },
],
valuesOnRows: true,
collapsed: true,
};
Default Layout vs Values On Rows
Section titled “Default Layout vs Values On Rows”Default layout:
rows: Regioncolumns: Quartervalues: Sales, MarginThis usually produces:
- one row branch per region
- one generated value column per quarter and measure
With valuesOnRows: true, the same measures become row members:
- Region
- Sales
- Margin
That is useful when:
- you have many measures and few row dimensions
- you want a more compact column area
- users compare measures vertically rather than horizontally
Configuration
Section titled “Configuration”const pivot: PivotConfig = { rows: ['region'], columns: ['quarter'], values: [ { prop: 'sales', aggregator: 'sum' }, { prop: 'margin', aggregator: 'avg' }, ], valuesOnRows: true,};Explicit Values Position
Section titled “Explicit Values Position”Use rowTree with the $values pseudo-field when measures need to appear at a specific hierarchy level:
rowTree: ['carModel', 'cellId', '$values']rowTree: ['carModel', '$values', 'cellId']rowTree: ['$values', 'carModel', 'cellId']rowTree is the explicit advanced API. If it is provided, it controls value placement even when valuesOnRows is also set.
What Changes Internally
Section titled “What Changes Internally”- Pivot adds a synthetic row field called
Values. - Generated value columns become value labels in the row hierarchy.
- Grouping and row drill-down now treat the value label as part of the row structure.
This is why row drill-down helpers account for valuesOnRows when deriving row props.
Tradeoffs
Section titled “Tradeoffs”Benefits:
- fewer generated columns
- easier comparison of multiple measures
- clearer layouts on narrow screens
Costs:
- more synthetic rows
- row drill-down can feel deeper because the value label becomes part of the hierarchy
- users expecting a spreadsheet-style “measures across columns” layout may find it less familiar
When Not To Use It
Section titled “When Not To Use It”Avoid valuesOnRows when:
- you only have one measure
- your users think in terms of wide analytical columns
- you already have a deep row hierarchy and want to keep rows compact
Continue with Drill-Down to see how row and column expansion behave in both layouts.