Zandi (HeatMap)
Preview
Code
Zandi
3μ
2μ
1μ
12μ
11μ
Tailwind μ€μΉ
tailwindCSS
Dependencies μ€μΉ
npx shadcn-ui@latest add popover
Component μμ±
ν΄λꡬ쑰
/zandi /components zandi.tsx zandi-item.tsx /utils index.ts /types index.ts index.ts
zandi/types/index.ts
export interface HeatMapData { date: string; value: number; } interface PaletteItem { threshold: number; color: string; } export type Palette = PaletteItem[];
zandi/utils/index.ts
import { HeatMapData } from '../types'; const formatDateStr = (date: Date): string => { return date.toISOString().split('T')[0]; }; export const generateDates = (startDate: Date, endDate: Date): Date[] => { const dates = []; for (let day = startDate; day <= endDate; day.setDate(day.getDate() + 1)) { dates.push(new Date(day)); } return dates; }; export const groupByWeeks = (dates: Date[], heatMapData: HeatMapData[]) => { const heatMapDataMap = new Map( heatMapData.map((item) => [item.date, item.value]) ); const weeks: { dates: HeatMapData[]; month: number; isFirstWeek: boolean; }[] = []; let week: HeatMapData[] = []; let lastMonth: number | null = null; dates.forEach((date, idx) => { const month = date.getMonth(); const dateStr = formatDateStr(date); const heatMapValue = heatMapDataMap.get(dateStr) || 0; if (date.getDay() === 0 && 0 < week.length) { weeks.push({ dates: week, month: month + 1, isFirstWeek: lastMonth !== month, }); week = []; lastMonth = month; } week.push({ date: dateStr, value: heatMapValue, }); if (idx === dates.length - 1) { weeks.push({ dates: week, month: month + 1, isFirstWeek: lastMonth !== month, }); } }); return weeks.reverse(); // μ΅μ λ°μ΄ν°κ° μλ‘κ°λλ‘ λ€μ§κΈ° };
zandi/components/zandi.tsx
import * as React from 'react'; import { HeatMapData, Palette } from '../types'; import { ZandiItem } from './zandi-item'; import { generateDates, groupByWeeks } from '../utils'; import { cn } from '@/lib/utils'; interface ZandiProps { heatMapData: HeatMapData[]; startDate: Date; endDate: Date; palette: Palette; locale?: string; className?: string; } export const Zandi = ({ heatMapData, startDate, endDate, palette, locale = 'default', className, }: ZandiProps) => { const dates = React.useMemo( () => generateDates(startDate, endDate), [startDate, endDate] ); const weeks = React.useMemo( () => groupByWeeks(dates, heatMapData), [dates, heatMapData] ); const dayHeaders = React.useMemo(() => { const format = new Intl.DateTimeFormat(locale, { weekday: 'short' }); return Array.from({ length: 7 }, (_, i) => format.format(new Date(1970, 0, i + 4)) ); }, [locale]); palette.sort((a, b) => a.threshold - b.threshold); return ( <div className={cn('relative flex flex-col gap-2', className)}> <div className="grid grid-cols-7 gap-2 mb-2 text-foreground/70 text-xs text-center"> {dayHeaders.map((day, index) => ( <div key={index}>{day}</div> ))} </div> {weeks.map((week, idx) => ( <div key={idx} className={'grid grid-cols-7 gap-2'}> {/* ν΄λΉ μ£Όμ λ¬μ΄ λ°λλ©΄ μΌμͺ½μ λ¬ νμ */} {week.isFirstWeek ? ( <p className="absolute -left-10 text-xs">{week.month}μ</p> ) : null} {week.dates.map((date, dateIdx) => ( <ZandiItem key={dateIdx} date={date} palette={palette} /> ))} </div> ))} </div> ); };
zandi/components/zandi-item.tsx
import { HeatMapData, Palette } from '../types'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; interface ZandiItemProps { date: HeatMapData; palette: Palette; } export const ZandiItem = ({ date, palette }: ZandiItemProps) => { const { value } = date; let backgroundColor = ''; for (let i = palette.length - 1; 0 <= i; --i) { if (palette[i].threshold <= value) { backgroundColor = palette[i].color; break; } } return ( <Popover> <PopoverTrigger> <div key={date.date} style={{ backgroundColor }} className="bg-[#262626] w-6 h-6 rounded-sm" /> </PopoverTrigger> <PopoverContent className="w-28 space-y-1"> <p className="text-sm font-semibold text-center">{date.value}</p> <p className="text-xs text-foreground/70 text-center">{date.date}</p> </PopoverContent> </Popover> ); };
zandi/index.ts
export { Zandi } from './components/zandi'; export { ZandiItem } from './components/zandi-item'; export * from './utils'; export * from './types';
interface ZandiProps { heatMapData: HeatMapData[]; // μλ λ°μ΄ν° startDate: Date; endDate: Date; palette: Palette; // νλ νΈμ 컬λ¬μ κ²½κ³ locale?: string; // Intl.DateTimeFormat props νμΈ ("default" | "en-US" | "ko-KR" ...) className?: string; // μΆκ° μ€νμΌ } interface PaletteItem { threshold: number; // value <= threshold λ©΄ ν΄λΉ color μλ νμ color: string; } export type Palette = PaletteItem[];
κ·Έ μΈ μ€νμΌλ§μ μ»΄ν¬λνΈμμ μ§μ (zandi.tsx, zandi-item.tsx)