# @smallwebco/tinypivot-vue

A lightweight data grid with free pivot tables, Pro charts, and optional AI-powered data exploration for Vue 3. **Under 50KB gzipped** — 10x smaller than AG Grid.

**[Live Demo](https://tiny-pivot.com)** · **[Buy License](https://tiny-pivot.com/#pricing)**

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/Small-Web-Co/tinypivot/tree/master/examples/stackblitz-vue)

## Why TinyPivot?

- **Lightweight**: Under 50KB gzipped vs 500KB+ for AG Grid
- **Free Pivot Tables**: Sum aggregations, totals, and calculated fields included
- **Pro Upgrade**: Advanced aggregations, charts, AI Data Analyst, and no watermark
- **AI Data Analyst** (Pro): Natural language queries with BYOK — use your own OpenAI/Anthropic key
- **Lifetime License**: No subscriptions — buy once, use forever

## Installation

```bash
pnpm add @smallwebco/tinypivot-vue
```

## Quick Start

```vue
<script setup lang="ts">
import { DataGrid } from '@smallwebco/tinypivot-vue'
import '@smallwebco/tinypivot-vue/style.css'

const data = [
  { id: 1, region: 'North', product: 'Widget A', sales: 12500, units: 150 },
  { id: 2, region: 'North', product: 'Widget B', sales: 8300, units: 95 },
  { id: 3, region: 'South', product: 'Widget A', sales: 15200, units: 180 },
  { id: 4, region: 'South', product: 'Widget B', sales: 9800, units: 110 },
]
</script>

<template>
  <DataGrid
    :data="data"
    :enable-export="true"
    :enable-search="true"
    :enable-pagination="true"
    :page-size="100"
    theme="light"
  />
</template>
```

## Theming

TinyPivot ships 22 themes — 2 neutral (`light`, `dark`) plus 10 brand themes each with a light and dark variant. Themes are applied via the `theme` prop on `DataGrid`.

### Quick start

```vue
<DataGrid :data="data" theme="slate-dark" />
```

`theme="auto"` resolves to `'light'` or `'dark'` based on the user's system preference (`prefers-color-scheme`).

### Available themes

| Theme | Accent | Vibe |
|---|---|---|
| `light` / `dark` / `auto` | indigo / violet | TinyPivot defaults — neutral cool grays |
| `slate` / `slate-dark` | `#4f46e5` indigo | Linear / Stripe — cool neutral |
| `zinc` / `zinc-dark` | near-mono | Vercel / Anthropic — minimalist |
| `indigo` / `indigo-dark` | `#6366f1` vivid indigo | Premium SaaS |
| `violet` / `violet-dark` | `#8b5cf6` purple | Data viz / AI tools |
| `emerald` / `emerald-dark` | `#10b981` green | Fintech / finance |
| `sky` / `sky-dark` | `#0ea5e9` light blue | Productivity / airy |
| `rose` / `rose-dark` | `#f43f5e` warm pink | Friendly / creator |
| `amber` / `amber-dark` | `#f59e0b` warm orange | Energy / wellness |
| `solar` / `solar-dark` | `#b58900` mustard | Solarized-inspired warm cream + dark teal |
| `mono` / `mono-dark` | `#000` / `#fff` | Editorial — pure grayscale, high contrast |

### Custom themes

Brand themes redefine ~25 CSS custom property tokens at the grid root. You can override these in your own CSS to create a custom theme:

```vue
<template>
  <DataGrid :data="data" theme="light" class="my-brand" />
</template>

<style>
.vpg-data-grid.my-brand {
  --vpg-accent: #ff6b35;
  --vpg-accent-hover: #e55426;
  --vpg-surface-bg: #fafaf7;
  --vpg-surface-panel: #f0eee7;
  /* …override any of the tokens below */
}
</style>
```

**Token reference** — these are the variables you can override:

- **Surfaces**: `--vpg-surface-bg`, `--vpg-surface-panel`, `--vpg-surface-elevated`, `--vpg-surface-hover`, `--vpg-surface-selected`, `--vpg-surface-striped`
- **Text**: `--vpg-text-primary`, `--vpg-text-secondary`, `--vpg-text-muted`, `--vpg-text-inverse`
- **Borders**: `--vpg-border-default`, `--vpg-border-strong`, `--vpg-border-subtle`
- **Accent**: `--vpg-accent`, `--vpg-accent-hover`, `--vpg-accent-soft-bg`, `--vpg-accent-soft-text`, `--vpg-focus-ring`
- **States**: `--vpg-state-error`, `--vpg-state-warning`, `--vpg-state-success`, `--vpg-state-info`
- **Scrollbar**: `--vpg-scrollbar-thumb`, `--vpg-scrollbar-track`

Custom theme classes layer on top of any preset, so you can start from `theme="dark"` and tweak just the accent, for example.

### Adding a theme switcher

Let users pick their own theme — bind a reactive `theme` value to a `<select>`:

```vue
<script setup lang="ts">
import { DataGrid } from '@smallwebco/tinypivot-vue'
import '@smallwebco/tinypivot-vue/style.css'
import { ref } from 'vue'

const theme = ref<'light' | 'dark' | 'slate' | 'slate-dark' | 'emerald' | 'emerald-dark'>('dark')
const data = [/* ... */]
</script>

<template>
  <select v-model="theme">
    <option value="light">Light</option>
    <option value="dark">Dark</option>
    <option value="slate-dark">Slate (dark)</option>
    <option value="emerald-dark">Emerald (dark)</option>
    <!-- …add more themes -->
  </select>

  <DataGrid :data="data" :theme="theme" />
</template>
```

## Features

| Feature | Free | Pro |
|---------|:----:|:---:|
| Excel-like data grid | ✅ | ✅ |
| Column filtering & sorting | ✅ | ✅ |
| Global search | ✅ | ✅ |
| CSV export | ✅ | ✅ |
| Pagination | ✅ | ✅ |
| Column resizing | ✅ | ✅ |
| Clipboard (Ctrl+C) | ✅ | ✅ |
| Dark mode | ✅ | ✅ |
| Pivot table with Sum aggregation | ✅ | ✅ |
| Row/column totals | ✅ | ✅ |
| Calculated fields with formulas | ✅ | ✅ |
| Pivot row group expand/collapse | ✅ | ✅ |
| **Pivot drill-through** (double-click to inspect source rows) | ❌ | ✅ |
| **Excel (XLSX) Export** (styled, multi-level pivot headers, lazy-loaded) | ❌ | ✅ |
| **AI Data Analyst** (natural language, BYOK) | ❌ | ✅ |
| **Chart Builder** (6 chart types) | ❌ | ✅ |
| Advanced aggregations (Count, Avg, Min, Max, Unique, Median, Std Dev, %) | ❌ | ✅ |
| No watermark | ❌ | ✅ |

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `data` | `Record<string, unknown>[]` | **required** | Array of data objects |
| `loading` | `boolean` | `false` | Show loading spinner |
| `fontSize` | `'xs' \| 'sm' \| 'base'` | `'xs'` | Font size preset |
| `showPivot` | `boolean` | `true` | Show pivot toggle |
| `enableExport` | `boolean` | `true` | Show the Export dropdown menu (CSV free / Excel .xlsx Pro) |
| `enableSearch` | `boolean` | `true` | Show global search |
| `enablePagination` | `boolean` | `false` | Enable pagination |
| `pageSize` | `number` | `50` | Rows per page |
| `enableColumnResize` | `boolean` | `true` | Drag to resize columns |
| `enableClipboard` | `boolean` | `true` | Ctrl+C to copy cells |
| `theme` | `string` | `'light'` | Color theme — see [Theming](#theming) for the full list (22 presets) |
| `numberFormat` | `'us' \| 'eu' \| 'plain'` | `'us'` | Number display format: US (1,234.56), EU (1.234,56), plain (1234.56) |
| `dateFormat` | `'us' \| 'eu' \| 'iso'` | `'iso'` | Date display format: US (MM/DD/YYYY), EU (DD/MM/YYYY), ISO (YYYY-MM-DD) |
| `fieldRoleOverrides` | `Record<string, FieldRole>` | `undefined` | Override auto-detected chart field roles per column (`'dimension'` \| `'measure'` \| `'temporal'`) |
| `enableDrillDown` | `boolean` | `true` | Enable pivot row group expand/collapse chevrons |
| `enableDrillThrough` | `boolean` | `true` | Enable double-click drill-through on pivot cells (Pro feature) |
| `pivotLayout` | `'grouped' \| 'tabular'` | `'grouped'` | Row layout for multi-field pivots: `'grouped'` merges repeated parent values into a spanning cell; `'tabular'` repeats every value on each row. |

## Events

| Event | Payload | Description |
|-------|---------|-------------|
| `@cell-click` | `{ row, col, value, rowData }` | Cell clicked |
| `@selection-change` | `{ cells, values }` | Selection changed |
| `@export` | `{ rowCount, filename }` | CSV exported |
| `@copy` | `{ text, cellCount }` | Cells copied |
| `@collapse-change` | `string[]` | Pivot row groups collapsed/expanded (array of collapsed path keys) |
| `@drill-through` | `DrillThroughResult` | Pivot cell double-clicked and drill-through modal opened (Pro) |

## Export

When `enableExport` is `true` (the default), the toolbar shows a single **Export** dropdown button. Clicking it opens a menu with two items:

- **CSV** — always available (free). Downloads a `.csv` file of the current view.
- **Excel (.xlsx)** — Pro only. Free users see this item disabled with a **Pro** badge. Pro users get a styled `.xlsx` download. `exceljs` (~250 KB) loads lazily via dynamic import — never part of your main bundle.

### CSV Export (Free)

```typescript
import { exportToCSV, exportPivotToCSV } from '@smallwebco/tinypivot-vue'

exportToCSV(data, columns, { filename: 'my-data.csv' })
```

### Excel (XLSX) Export (Pro)

```typescript
import { exportToXLSX, exportPivotToXLSX } from '@smallwebco/tinypivot-vue'

// Flat grid
await exportToXLSX(data, columns, {
  filename: 'report.xlsx',
  sheetName: 'Sales',
  numberFormats: { revenue: '#,##0.00' },
})

// Pivot table
await exportPivotToXLSX(pivotData, rowFields, columnFields, valueFields, {
  filename: 'pivot.xlsx',
})
```

#### Pivot XLSX: two-sheet workbook

Pivot XLSX export produces a **two-sheet workbook**:

1. **Pivot** — the styled pivot summary (merged column headers, bold totals row, frozen header).
2. **Source Data** — the underlying source rows as an **interactive Excel Table** (filter/sort dropdowns, `TableStyleMedium2`). Open it in Excel and choose **Insert → PivotTable** to build a native PivotTable in two clicks.

> Note: TinyPivot does not generate a native Excel PivotTable object (exceljs does not support writing pivot table XML). The Source Data sheet is the practical alternative.
```

## Pivot Drill-Down

### Row Group Expand/Collapse (Free)

When a pivot has two or more row fields, each group row displays a `▸`/`▾` chevron. Click to collapse or expand that group. Alt-click collapses/expands every group at the same depth. Collapsed groups still show correct aggregated values over all rows in the group.

By default (`pivot-layout="grouped"`), the parent row value is rendered once as a vertically spanning cell across its children, making the hierarchy immediately readable. Set `:pivot-layout="'tabular'"` to repeat the parent value on every row instead.

```vue
<template>
  <DataGrid
    :data="data"
    @collapse-change="(collapsedPaths) => console.log('Collapsed:', collapsedPaths)"
  />
</template>
```

### Drill-Through to Source Rows (Pro)

Double-click any pivot value cell (including totals) to open a modal with the underlying source rows. Includes a slice description header, paginated table (50/page), and CSV export. Requires a Pro license.

```vue
<template>
  <DataGrid
    :data="data"
    @drill-through="({ rows, descriptor }) => console.log(`${descriptor.rowCount} rows`)"
  />
</template>
```

Disable either behaviour individually:

```vue
<DataGrid :data="data" :enable-drill-down="false" :enable-drill-through="false" />
```

## AI Data Analyst (Pro)

Optional AI-powered data analyst that lets users explore data using natural language. Ask questions like "What's the return rate by category?" and get instant results.

> See the [AI Analyst Demo](https://github.com/Small-Web-Co/tinypivot/tree/master/examples/ai-analyst-demo) for a complete working example.

```vue
<script setup lang="ts">
import { DataGrid } from '@smallwebco/tinypivot-vue'
import '@smallwebco/tinypivot-vue/style.css'

const data = [/* your data */]

const aiConfig = {
  enabled: true,
  aiEndpoint: '/api/ai-proxy',           // Your AI proxy endpoint
  databaseEndpoint: '/api/tp-database',  // Optional: auto-discover tables
  aiModelName: 'Claude Sonnet 4',        // Optional: display in UI
  persistToLocalStorage: true,           // Preserve conversation on tab switch
}
</script>

<template>
  <DataGrid
    :data="data"
    :ai-analyst="aiConfig"
  />
</template>
```

### AI Analyst Config Options

| Option | Type | Description |
|--------|------|-------------|
| `enabled` | `boolean` | Enable the AI Analyst tab |
| `aiEndpoint` | `string` | Your AI proxy endpoint (keeps API keys secure) |
| `databaseEndpoint` | `string` | Unified endpoint for table discovery and queries |
| `dataSources` | `AIDataSource[]` | Manual list of available tables |
| `queryExecutor` | `function` | Custom query executor (e.g., client-side DuckDB) |
| `aiModelName` | `string` | Display name for the AI model in UI |
| `persistToLocalStorage` | `boolean` | Persist conversation across tab switches |
| `sessionId` | `string` | Unique session ID for conversation isolation |
| `maxRows` | `number` | Max rows to return (default: 10000) |
| `demoMode` | `boolean` | Use canned responses (no real AI calls) |

### AI Analyst Events

| Event | Payload | Description |
|-------|---------|-------------|
| `@ai-data-loaded` | `{ data, query, rowCount }` | Query results loaded |
| `@ai-conversation-update` | `{ conversation }` | Conversation state changed |
| `@ai-query-executed` | `{ query, rowCount, duration, success }` | SQL query executed |
| `@ai-error` | `{ message, type }` | Error occurred |

### State Preservation

The AI Analyst preserves state when switching between tabs (Grid, Pivot, Chart, AI):

- **Conversation history** is maintained in memory
- **Query results** are preserved
- **SQL queries** remain accessible via the SQL panel

To persist across page refreshes, enable `persistToLocalStorage: true`. The conversation will be saved to localStorage using the `sessionId` as the key.

For production apps, listen to `@ai-conversation-update` to implement your own persistence:

```vue
<template>
  <DataGrid
    :data="data"
    :ai-analyst="aiConfig"
    @ai-conversation-update="saveConversation"
  />
</template>

<script setup>
function saveConversation({ conversation }) {
  // Save to your backend
  api.saveConversation(userId, conversation)
}
</script>
```

## Documentation

See the [full documentation](https://github.com/Small-Web-Co/tinypivot) for complete API reference, styling, and Pro license activation.

## License

- **Free Tier**: MIT License for core grid and pivot features
- **Pro Features**: Commercial license required

**[Purchase at tiny-pivot.com/#pricing](https://tiny-pivot.com/#pricing)**

---

Built with ❤️ by [Small Web, LLC](https://thesmallweb.co)
