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.

Client Only

Installation

pnpm dlx @docyrus/cli add @docyrus/ui-pivot-calendar
Required Packages(1 package)
pnpm add date-fns
UI Primitives(5 components)
npx shadcn@latest add button checkbox scroll-area tabs tooltip

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:

ViewLayoutBucketsUse it for
month-calendarStandard month gridPer day (yyyy-MM-dd)Glanceable monthly overview
days-of-weekPivot grid · groups × days of selected weekPer day (yyyy-MM-dd)Weekly comparison across categories
days-of-monthPivot grid · groups × days of selected month, scrolls horizontallyPer day (yyyy-MM-dd)Detailed monthly comparison
months-of-yearPivot grid · groups × months of selected yearPer 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:

<PivotCalendar
  mode="local"
  data={timeEntries}
  getDate={row => 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:

<PivotCalendar
  mode="remote"
  cells={cellsFromServer}            // [{ bucket: '2026-05-06', groupId: 'u1', values: [...] }, ...]
  measures={measures}
  groupBy={{ id: 'user', label: 'User', getId: row => 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 <button> elements with hover / focus / "Click to drill down" tooltip styling whenever a handler is wired.

<PivotCalendar
  mode="remote"
  cells={cells}
  measures={measures}
  groupBy={{ id: 'user', label: 'User', getId: row => row.user.id }}
  groups={users}
  onCellClick={(info) => {
    console.log(info.bucket, info.bucketStart, info.bucketEnd);
    console.log(info.measure.label, info.value, info.formattedValue);
    console.log(info.groupId, info.group?.label);
  }}
/>

The component itself is intentionally agnostic about what to do with the click — it does not ship a drilldown dialog. Pair it with useDocyrusPivotCalendar and its buildDrilldownQuery helper for Docyrus-backed apps, or build your own dialog around any data source.

IPivotCalendarCellClick

FieldTypeDescription
bucketstringBucket key (yyyy-MM-dd or yyyy-MM).
bucketStartDateInclusive start of the bucket's range.
bucketEndDateInclusive end of the bucket's range (end-of-day or end-of-month).
viewTPivotCalendarViewView active at the time of the click.
groupIdstring | nullGroup id when groupBy is configured. null for the "Totals" row or ungrouped totals.
groupIPivotCalendarGroup | undefinedResolved group entry when available.
measureIPivotCalendarMeasure<TData>Measure descriptor for the clicked tile.
valuenumberRaw aggregated value.
formattedValuestringPre-formatted display value (e.g. 1.5h).

Usage

import {
  PivotCalendar,
  type IPivotCalendarMeasure,
  type IPivotCalendarRemoteCell,
  type TPivotCalendarMode
} from '@docyrus/ui/components/pivot-calendar';

const measures: Array<IPivotCalendarMeasure<TimeEntry>> = [
  { id: 'planned', label: 'Planned', aggregate: 'sum', getValue: r => r.duration_planned, formatValue: s => `${(s/3600).toFixed(1)}h` },
  { id: 'logged',  label: 'Logged',  aggregate: 'sum', getValue: r => r.duration_logged,  formatValue: s => `${(s/3600).toFixed(1)}h` }
];

<PivotCalendar
  mode="local"
  data={data}
  getDate={row => row.date}
  measures={measures}
  groupBy={{ id: 'user', label: 'User', getId: row => row.user.id }}
  groups={users}
  defaultView="month-calendar"
/>

Sizes

SizeDescription
smCompact — min-h-[480px]
defaultDefault — min-h-[600px]
lgTall — min-h-[720px]

API Reference

PropTypeDefaultDescription
mode'local' | 'remote''local'Calculation mode.
dataArray<TData>—Raw rows. Required when mode='local'.
cellsArray<IPivotCalendarRemoteCell>—Pre-aggregated cells. Required when mode='remote'.
getDate(row: TData) => Date | string—Reads the date for a row. Required when mode='local'.
measuresArray<IPivotCalendarMeasure<TData>>—Measures to display. Up to 4 are rendered per cell.
groupByIPivotCalendarGroupBy<TData>—Optional grouping category. Enables the side panel + pivot row axis.
groupsArray<IPivotCalendarGroup>—Group catalog (required for mode='remote' with groupBy).
defaultViewTPivotCalendarView'month-calendar'Initial view.
viewTPivotCalendarView—Controlled view value.
onViewChange(view: TPivotCalendarView) => void—Called when view changes.
defaultDateDatenew Date()Initial reference date.
dateDate—Controlled reference date.
onDateChange(date: Date) => void—Called when reference date changes.
defaultSelectedGroupIdsArray<string>AllInitial selected groups.
selectedGroupIdsArray<string>—Controlled group selection.
onSelectedGroupIdsChange(ids: Array<string>) => void—Called when group selection changes.
visibleViewsArray<TPivotCalendarView>AllViews shown in the toolbar tabs.
hideViewSwitcherbooleanfalseHide the view switcher tabs.
hideSidebarbooleanfalseHide the group side panel (only used by month-calendar).
titleReactNoderange labelCustom toolbar title.
onCellClick(info: IPivotCalendarCellClick<TData>) => void—Fires when the user clicks a stat tile or pivot value cell. Tiles become interactive (hover ring + focus outline + drill-down hint) only when this prop is set.
size'sm' | 'default' | 'lg''default'Min height variant.
classNamestring—Extra wrapper classes.

Components

ComponentDescription
PivotCalendarPublic root component
pivotCalendarVariantsCVA helper for the wrapper container
usePivotCalendarControllerStandalone controller hook (advanced compositions)

Type Exports

TypeDescription
PivotCalendarProps<TData>Component prop shape
PivotCalendarRootProps<TData>Root component (props + size variant)
IPivotCalendarController<TData>Controller returned by the hook
IPivotCalendarMeasure<TData>Measure descriptor
IPivotCalendarMeasureValueA measure result { measureId, value }
IPivotCalendarGroupGroup entry used in the sidebar / row axis
IPivotCalendarGroupBy<TData>Grouping dimension descriptor
IPivotCalendarRemoteCellPre-aggregated cell { bucket, groupId?, values }
IPivotCalendarCellClick<TData>Payload emitted by onCellClick when a stat tile is clicked
TPivotCalendarViewView id
TPivotCalendarMode'local' | 'remote'
TPivotCalendarAggregate'sum' | 'count' | 'avg' | 'min' | 'max'
TPivotCalendarBucketKind'day' | 'weekday' | 'month'

Type Reference

IPivotCalendarMeasure

FieldTypeDescription
idstringUnique measure id
labelstringTooltip label
shortLabelstringCompact label shown in the cell (defaults to label)
aggregateTPivotCalendarAggregateAggregation strategy used in local mode
getValue(row: TData) => number | null | undefinedReads the numeric value from a row
formatValue(value: number) => stringFormats the displayed value (e.g. 1.5h)
colorstringCSS color of the dot accent
descriptionstringExtra context shown in the tooltip

IPivotCalendarRemoteCell

FieldTypeDescription
bucketstringyyyy-MM-dd (day views) or yyyy-MM (month view)
groupIdstring | nullGroup id when groupBy is configured
valuesArray<IPivotCalendarMeasureValue>One entry per measure

TPivotCalendarView

'month-calendar' \| 'days-of-week' \| 'days-of-month' \| 'months-of-year'

On this page