Pivot chart

One drag-drop pivot layout, two synced views: <SvPivotDesigner chartable> renders the SAME Rows / Columns / Values as either an expandable pivot grid or a chart (Columns -> series, Values -> measure). Powered by the enterprise pivot engine. (requires @svgrid/enterprise)

A live, editable Svelte 5 data grid example from the SvGrid gallery (Pivot Grid). See the SvGrid documentation for the full API.

About this example

One drag-and-drop pivot layout rendered two ways in Svelte 5. SvPivotDesigner with the chartable prop renders the same Rows, Columns and Values as either an expandable pivot grid or a live chart, switched with a Table and Chart toggle in the toolbar: rows become chart categories with their full path so nested rows read Americas, Canada, columns become series and the values the measure. The enterprise pivot engine feeds pivotToChartSpec into the MIT SvGridChart.

One drag-drop pivot layout, two views. <SvPivotDesigner chartable> renders the SAME layout (Rows / Columns / Values) as either an expandable pivot grid or a live chart - flip Table <-> Chart in the toolbar. Rows -> chart categories (full path, so nested rows read "Americas · Canada"), Columns -> series, Values -> the measure. Powered by the enterprise pivot engine (createPivotModel -> pivotToChartSpec) over the free MIT SvGridChart.

Imports, features and API used

Imports: @svgrid/enterprise

Frequently asked questions

How do the wells map to the chart?

Each row path becomes a category on the axis, each column-axis member becomes a series and the first value well entry is the measure, so a Region by Quarter revenue pivot becomes grouped bars per region with one bar per quarter.

Is the chart component enterprise?

No. SvGridChart is in the MIT @svgrid/grid package; the pivot engine and pivotToChartSpec that feed it are in @svgrid/enterprise.

Does switching views lose the layout?

No. Both views read the same bound layout, so a change in the wells updates the table and the chart alike.

Related documentation

Related articles

Source code (359-pivot-chart.svelte)

<script lang="ts">
  /**
   * 359. Pivot chart (Enterprise) - the Excel PivotTable <-> PivotChart pairing
   * ---------------------------------------------------------------------------
   * One drag-drop pivot layout, two views. `<SvPivotDesigner chartable>` renders
   * the SAME layout (Rows / Columns / Values) as either an expandable pivot grid
   * or a live chart - flip Table <-> Chart in the toolbar. Rows -> chart
   * categories (full path, so nested rows read "Americas · Canada"), Columns ->
   * series, Values -> the measure. Powered by the enterprise pivot engine
   * (createPivotModel -> pivotToChartSpec) over the free MIT SvGridChart.
   */
  import { SvPivotDesigner, type PivotField, type PivotLayout, type PivotPreset } from '@svgrid/enterprise'

  type Row = {
    id: number
    region: 'Americas' | 'EMEA' | 'APAC'
    country: string
    category: 'Electronics' | 'Apparel' | 'Home' | 'Outdoors'
    quarter: 'Q1' | 'Q2' | 'Q3' | 'Q4'
    salesperson: string
    units: number
    revenue: number
    profit: number
  }
  const REGIONS: Row['region'][] = ['Americas', 'EMEA', 'APAC']
  const COUNTRIES: Record<Row['region'], string[]> = {
    Americas: ['USA', 'Canada', 'Brazil', 'Mexico'],
    EMEA: ['UK', 'Germany', 'France', 'Spain'],
    APAC: ['Japan', 'Australia', 'Singapore', 'India'],
  }
  const CATEGORIES: Row['category'][] = ['Electronics', 'Apparel', 'Home', 'Outdoors']
  const QUARTERS: Row['quarter'][] = ['Q1', 'Q2', 'Q3', 'Q4']
  const NAMES = ['Ada', 'Grace', 'Linus', 'Donald', 'Margaret', 'Brian', 'Barbara', 'Ken']
  let seed = 0xc0ffee
  const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 0xffffffff)
  const pick = <T,>(list: readonly T[]): T => list[Math.floor(rnd() * list.length)]!
  const rows: Row[] = Array.from({ length: 900 }, (_, id) => {
    const region = REGIONS[id % 3]!
    const country = pick(COUNTRIES[region])
    const category = pick(CATEGORIES)
    const price = category === 'Electronics' ? 280 : category === 'Apparel' ? 65 : category === 'Home' ? 120 : 45
    const units = Math.round(2 + rnd() * 38)
    const revenue = Math.round(units * price * (0.8 + rnd() * 0.5))
    return {
      id, region, country, category, quarter: pick(QUARTERS),
      salesperson: NAMES[id % NAMES.length]!,
      units, revenue,
      profit: Math.round(revenue * (0.15 + rnd() * 0.25)),
    }
  })

  const money = { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } } as const
  const fields: PivotField<Row>[] = [
    { field: 'region', label: 'Region', kind: 'dimension', group: 'Geography' },
    { field: 'country', label: 'Country', kind: 'dimension', group: 'Geography' },
    { field: 'category', label: 'Category', kind: 'dimension', group: 'Product' },
    { field: 'quarter', label: 'Quarter', kind: 'dimension', group: 'Time' },
    { field: 'salesperson', label: 'Salesperson', kind: 'dimension', group: 'People' },
    { field: 'units', label: 'Units', kind: 'measure', defaultAgg: 'sum', format: { type: 'number', options: { maximumFractionDigits: 0 } } },
    { field: 'revenue', label: 'Revenue', kind: 'measure', defaultAgg: 'sum', format: money },
    { field: 'profit', label: 'Profit', kind: 'measure', defaultAgg: 'sum', format: money },
  ]

  const presets: PivotPreset[] = [
    {
      name: 'Region → Country × Quarter',
      layout: { rows: ['region', 'country'], cols: ['quarter'], values: [{ field: 'revenue', agg: 'sum', label: 'Revenue', format: money }], filters: [] },
    },
    {
      name: 'Region × Category (revenue + profit)',
      layout: { rows: ['region'], cols: ['category'], values: [{ field: 'revenue', agg: 'sum', label: 'Revenue', format: money }, { field: 'profit', agg: 'sum', label: 'Profit', format: money }], filters: [] },
    },
    {
      name: 'Salesperson scorecard',
      layout: { rows: ['salesperson'], cols: [], values: [{ field: 'revenue', agg: 'sum', label: 'Revenue', format: money }, { field: 'units', agg: 'avg', label: 'Avg units' }], filters: [] },
    },
  ]

  // Default: Region -> Country (nested, so the grid expands / collapses) x Quarter.
  let layout = $state<PivotLayout>(presets[0]!.layout)
</script>

<div class="demo-page" style="height: 660px; display: flex; flex-direction: column;">
  <p class="demo-lede">
    One pivot layout, two views. The grid is <strong>expandable</strong> (click a Region to reveal its
    countries); flip <strong>Table &harr; Chart</strong> in the toolbar and the SAME layout draws as a live
    chart - Rows become <strong>categories</strong>, Columns <strong>series</strong>, Values the
    <strong>measure</strong>. Drag fields between wells, or try a preset.
  </p>
  <div style="flex: 1; min-height: 0;">
    <SvPivotDesigner
      data={rows}
      {fields}
      bind:layout
      {presets}
      expandable
      chartable
      defaultView="table"
    />
  </div>
</div>

View this example on GitHub

More Pivot Grid examples

  • Pivot mode grid - Built on <SvPivotDesigner panelPosition="right">: a docked tool panel with a PIVOT MODE toggle, field checklist, and Columns / Rows / Values wells (drag-and-drop). OFF renders the flat participant grid (column groups, flags, ratings, inline filter row); ON pivots Language -> Country x Game with heat-mapped avg measures.
  • Pivot - Table + Designer - Drag-and-drop Pivot Designer with Filters / Rows / Columns / Values zones, multi-level column headers, subtotal + grand-total rows, row-header sort menu.
  • Pivot - Sales pipeline - Polished pivot view: KPI strip, region/sales-person rows, quarter columns, two measures, expand-all/collapse-all toolbar, heatmap tinting.
  • Pivot - Conditional cells - Function-valued cell and header templates on top of createPivotModel: traffic-light revenue pills, target chips, units data-bars, measure icons in headers, region color dots in row labels.
  • Pivot - Drill-through - Click any pivot value cell - leaf, subtotal, or grand total - and the right rail opens with the source facts behind the aggregate. Total + count + average always match the cell.