Contribution Graph
navdeep-singh/contribution-graphFreeComponent
Clean contribution calendar with built-in normalization, keyboard navigation, and responsive latest-day visibility.
Install it
npx shadcn@latest add https://ui.navdeepsingh.dev/r/contribution-graph.jsonSource
1'use client'23import type { ComponentPropsWithoutRef, KeyboardEvent } from 'react'45import { cn } from '@/lib/utils'67export type ContributionDay = {8 date: string9 count: number10 level: 0 | 1 | 2 | 3 | 411}1213export type ContributionInputDay = Omit<ContributionDay, 'level'> & {14 level?: ContributionDay['level']15}1617export type ContributionGraphData = {18 days: readonly ContributionInputDay[]19 total?: number20 from?: string21 to?: string22 source?: string23}2425export type NormalizedContributionGraphData = {26 days: readonly ContributionDay[]27 total: number28 from: string29 to: string30 source?: string31}3233export type ContributionGraphProps = Omit<ComponentPropsWithoutRef<'figure'>, 'children'> & {34 data: ContributionGraphData35 title?: string36 description?: string37 showHeader?: boolean38 emptyMessage?: string39}4041const levelClassNames = [42 'bg-muted/55',43 'bg-foreground/20',44 'bg-foreground/40',45 'bg-foreground/65',46 'bg-foreground',47] as const4849const dateFormatter = new Intl.DateTimeFormat('en', {50 month: 'short',51 day: 'numeric',52 year: 'numeric',53 timeZone: 'UTC',54})55const monthFormatter = new Intl.DateTimeFormat('en', {56 month: 'short',57 timeZone: 'UTC',58})5960function formatDate(date: string) {61 return dateFormatter.format(new Date(`${date}T00:00:00Z`))62}6364function toDateString(timestamp: number) {65 return new Date(timestamp).toISOString().slice(0, 10)66}6768function getDerivedLevel(count: number, maximum: number): ContributionDay['level'] {69 if (count <= 0) return 070 return Math.min(4, Math.max(1, Math.ceil((count / Math.max(maximum, 1)) * 4))) as 1 | 2 | 3 | 471}7273/**74 * Turns provider data shaped as `{ date, count }` into a complete daily calendar.75 * Missing dates are filled with zeroes and intensity levels are derived when omitted.76 */77export function createContributionGraphData(78 inputDays: readonly ContributionInputDay[],79 options: { source?: string } = {},80): NormalizedContributionGraphData {81 const countsByDate = new Map<string, ContributionInputDay>()8283 for (const day of inputDays) {84 if (!/^\d{4}-\d{2}-\d{2}$/.test(day.date) || !Number.isFinite(day.count)) continue8586 const previous = countsByDate.get(day.date)87 const count = Math.max(0, Math.round(day.count)) + (previous?.count ?? 0)88 countsByDate.set(day.date, {89 date: day.date,90 count,91 level:92 day.level === undefined && previous?.level === undefined93 ? undefined94// … truncated
Files it writes
More from navdeep-singh
All 64 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Animated Groupanimated-group | navdeep-singh | Components | Free | 1 dep | |
| Animated Numbersanimated-numbers | navdeep-singh | Components | Free | 1 dep | |
| Animated Tabsanimated-tabs | navdeep-singh | Components | Free | 2 deps | |
| Approval Panel Illustrationapproval-panel | navdeep-singh | Components | Free | 2 deps · 2 files | |
| Brand Logobrand-logo | navdeep-singh | Components | Free | 1 dep | |
| Designed for Focus Illustrationdesigned-for-focus-illustration | navdeep-singh | Components | Free | 1 dep | |
| Expandable Cardexpandable-card | navdeep-singh | Components | Free | 4 deps | |
| Fast by Default Illustrationfast-by-default-illustration | navdeep-singh | Components | Free | 1 dep |
