# Pivot Calendar URL: /docs/web/components/pivot-calendar Calendar component that pivots time-based statistics across four layouts — full month, days of a week, days of a month, or months of a year — with grouping, tooltips, and local or remote aggregation. **Demo:** ```tsx 'use client'; // @custom-demo import { useMemo, useState } from 'react'; import { PivotCalendar, type IPivotCalendarGroup, type IPivotCalendarMeasure, type IPivotCalendarRemoteCell, type TPivotCalendarMode } from '@docyrus/ui/components/pivot-calendar'; import { useMounted } from '@docyrus/ui/primitives/hooks/use-mounted'; import { Tabs, TabsList, TabsTrigger } from '@docyrus/ui/primitives/ui/tabs'; import { cn } from '@docyrus/ui/primitives/lib/utils'; import { buildAggregatedCells, generateWorkTimeEntries, USERS, type WorkTimeEntry } from '@/data/pivot-calendar-data'; function formatHours(seconds: number): string { if (!seconds) return '0h'; const hours = seconds / 3600; const rounded = Math.round(hours * 10) / 10; return `${rounded}h`; } const DEMO_MEASURES: Array {mode === 'local' ? ( className="w-full" mode="local" data={localData} getDate={row => row.date} measures={DEMO_MEASURES} groupBy={{ id: 'user', label: 'User', getId: row => row.user.id, getGroup: (row) => { const user = USERS.find(u => u.id === row.user.id); return { id: row.user.id, label: row.user.name, image: row.user.photo ? { signed_url: row.user.photo, file_name: row.user.name } : null, color: user?.color }; } }} groups={groups} defaultView="month-calendar" /> ) : ( className="w-full" mode="remote" cells={remoteCells} measures={DEMO_MEASURES} groupBy={{ id: 'user', label: 'User', getId: row => row.user.id }} groups={groups} defaultView="month-calendar" /> )} ); } ``` ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/ui-pivot-calendar ``` **Dependencies:** - [lucide-react](https://www.npmjs.com/package/lucide-react) - [date-fns](https://www.npmjs.com/package/date-fns) - [class-variance-authority](https://www.npmjs.com/package/class-variance-authority) ## Overview `PivotCalendar` represents statistical totals on top of a calendar surface. It supports up to **four measures per cell** rendered as a 2×1 or 2×2 micro-grid with hover tooltips, an optional grouping category sidebar (used as filters in the month view, as row axis on the pivot grid views), and four visual layouts: | View | Layout | Buckets | Use it for | |------|--------|---------|------------| | `month-calendar` | Standard month grid | Per day (`yyyy-MM-dd`) | Glanceable monthly overview | | `days-of-week` | Pivot grid · groups × days of selected week | Per day (`yyyy-MM-dd`) | Weekly comparison across categories | | `days-of-month` | Pivot grid · groups × days of selected month, scrolls horizontally | Per day (`yyyy-MM-dd`) | Detailed monthly comparison | | `months-of-year` | Pivot grid · groups × months of selected year | Per month (`yyyy-MM`) | Long-term yearly trend | ## Calculation modes The `mode` prop controls where aggregation happens. ### Local mode Pass raw rows. The component buckets and aggregates each measure on the client based on `getDate` and the optional `groupBy.getId`: ```tsx row.date} measures={[ { id: 'planned', label: 'Planned', aggregate: 'sum', getValue: r => r.duration_planned, formatValue: formatHours }, { id: 'logged', label: 'Logged', aggregate: 'sum', getValue: r => r.duration_logged, formatValue: formatHours } ]} groupBy={{ id: 'user', label: 'User', getId: row => row.user.id, getGroup: row => ({ id: row.user.id, label: row.user.name, image: { signed_url: row.user.photo } }) }} /> ``` ### Remote mode Pass pre-aggregated `cells` keyed by bucket and group. The component only renders, never calculates: ```tsx row.user.id }} groups={users} /> ``` The bucket key must match the active view: `yyyy-MM-dd` for day-based views (`month-calendar`, `days-of-week`, `days-of-month`) and `yyyy-MM` for `months-of-year`. ## Drill down on stat clicks `PivotCalendar` exposes an `onCellClick` callback that fires whenever the user clicks a measure tile (in the calendar month view) or a value cell (in any pivot grid view). Tiles with a zero value stay non-interactive; tiles with a value render as `