Skip to content

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:

rows: Region
columns: Quarter
values: Sales, Margin

This 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
const pivot: PivotConfig = {
rows: ['region'],
columns: ['quarter'],
values: [
{ prop: 'sales', aggregator: 'sum' },
{ prop: 'margin', aggregator: 'avg' },
],
valuesOnRows: true,
};

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.

  • 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.

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

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.