components
zandi
Zandi (HeatMap)
Zandi (HeatMap)
Required
Tailwind μ€μΉ
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';
How to customize Zandi?
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)