CalendarHeatmap

Data Visualization

깃허브 잔디 스타일 달력 히트맵.

Usage

날짜별 활동량이나 빈도를 달력 격자의 색 농도로 살펴볼 때 사용한다.

import

import

import { CalendarHeatmap } from '@mildang/design-system/Chart';

예제를 복사해 쓸 때 필요한 준비

@mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.

API Reference

CalendarHeatmap Props

Prop

Type

Default

data
필수

TDatum[]

지정 안 함

dateKey
필수

string

지정 안 함

valueKey
필수

string

지정 안 함

className

string

지정 안 함

colorRange

string[]

지정 안 함

emptyColor

string

token('colors.neutral.surface.high')

legendPosition

"bottom" | "left" | "right" | "top"

bottom

margin

Partial<ChartMargin>

지정 안 함

monthLabelFormat

(date: Date) => string

(date) => `${date.getMonth() + 1}월`

showLegend

boolean

false

showTooltip

boolean

false

valueLabel

string

지정 안 함

weekdayLabels

string[]

['일', '월', '화', '수', '목', '금', '토']

예제

데이터 1개

import { CalendarHeatmap } from '@mildang/design-system/Chart';

const CalendarHeatmapSingleDatumExample = () => (
    <CalendarHeatmap
      data={[{ date: '2026-03-02', solvedCount: 5 }]}
      dateKey="date"
      valueKey="solvedCount"
      valueLabel="푼 문제 수"
      showTooltip
    />
  );

export default CalendarHeatmapSingleDatumExample;

빈 데이터

import { CalendarHeatmap } from '@mildang/design-system/Chart';

const CalendarHeatmapEmptyExample = () => (
    <CalendarHeatmap data={[]} dateKey="date" valueKey="solvedCount" showLegend />
  );

export default CalendarHeatmapEmptyExample;

기본

import { CalendarHeatmap } from '@mildang/design-system/Chart';

function deterministicJitter(seed: string): number {
  let h = 1779033703 ^ seed.length;
  for (let i = 0; i < seed.length; i += 1) {
    h = Math.imul(h ^ seed.charCodeAt(i), 3432918353);
    h = (h << 13) | (h >>> 19);
  }
  h = Math.imul(h ^ (h >>> 16), 2246822507);
  h = Math.imul(h ^ (h >>> 13), 3266489909);
  return (h >>> 0) / 4294967296 - 0.5;
}

const pad = (n: number) => String(n).padStart(2, '0');

/** 시작일부터 days일간의 학습 기록 — deterministicJitter 기반 결정적 샘플 (Math.random 금지) */
function buildStudyLog(start: { year: number; month: number; day: number }, days: number) {
  const rows: { date: string; solvedCount: number }[] = [];
  for (let i = 0; i < days; i += 1) {
    const d = new Date(start.year, start.month - 1, start.day + i);
    const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
    const level = Math.round((deterministicJitter(date) + 0.5) * 10);
    // 일부 날짜는 0 (학습하지 않은 날)
    rows.push({ date, solvedCount: Math.max(0, level - 2) * 4 });
  }
  return rows;
}

// 2026년 1~6월 학생 학습 기록 (푼 문제 수)
const studyLog = buildStudyLog({ year: 2026, month: 1, day: 5 }, 180);

const CalendarHeatmapDefaultExample = () => (
    <CalendarHeatmap
      data={studyLog}
      dateKey="date"
      valueKey="solvedCount"
      valueLabel="푼 문제 수"
      showLegend
    />
  );

export default CalendarHeatmapDefaultExample;

Hover Tooltip

import { CalendarHeatmap } from '@mildang/design-system/Chart';

function deterministicJitter(seed: string): number {
  let h = 1779033703 ^ seed.length;
  for (let i = 0; i < seed.length; i += 1) {
    h = Math.imul(h ^ seed.charCodeAt(i), 3432918353);
    h = (h << 13) | (h >>> 19);
  }
  h = Math.imul(h ^ (h >>> 16), 2246822507);
  h = Math.imul(h ^ (h >>> 13), 3266489909);
  return (h >>> 0) / 4294967296 - 0.5;
}

const pad = (n: number) => String(n).padStart(2, '0');

/** 시작일부터 days일간의 학습 기록 — deterministicJitter 기반 결정적 샘플 (Math.random 금지) */
function buildStudyLog(start: { year: number; month: number; day: number }, days: number) {
  const rows: { date: string; solvedCount: number }[] = [];
  for (let i = 0; i < days; i += 1) {
    const d = new Date(start.year, start.month - 1, start.day + i);
    const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
    const level = Math.round((deterministicJitter(date) + 0.5) * 10);
    // 일부 날짜는 0 (학습하지 않은 날)
    rows.push({ date, solvedCount: Math.max(0, level - 2) * 4 });
  }
  return rows;
}

// 2026년 1~6월 학생 학습 기록 (푼 문제 수)
const studyLog = buildStudyLog({ year: 2026, month: 1, day: 5 }, 180);

const tooltipLog = studyLog.map((row) =>
  row.date === '2026-03-02' ? { ...row, solvedCount: 1234 } : row,
);

const CalendarHeatmapHoverTooltipExample = () => (
    <CalendarHeatmap
      data={tooltipLog}
      dateKey="date"
      valueKey="solvedCount"
      valueLabel="푼 문제 수"
      showTooltip
    />
  );

export default CalendarHeatmapHoverTooltipExample;

커스텀 색상·라벨

import { CalendarHeatmap } from '@mildang/design-system/Chart';
import { token } from '@mildang/styled-system/tokens';

function deterministicJitter(seed: string): number {
  let h = 1779033703 ^ seed.length;
  for (let i = 0; i < seed.length; i += 1) {
    h = Math.imul(h ^ seed.charCodeAt(i), 3432918353);
    h = (h << 13) | (h >>> 19);
  }
  h = Math.imul(h ^ (h >>> 16), 2246822507);
  h = Math.imul(h ^ (h >>> 13), 3266489909);
  return (h >>> 0) / 4294967296 - 0.5;
}

const pad = (n: number) => String(n).padStart(2, '0');

/** 시작일부터 days일간의 학습 기록 — deterministicJitter 기반 결정적 샘플 (Math.random 금지) */
function buildStudyLog(start: { year: number; month: number; day: number }, days: number) {
  const rows: { date: string; solvedCount: number }[] = [];
  for (let i = 0; i < days; i += 1) {
    const d = new Date(start.year, start.month - 1, start.day + i);
    const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
    const level = Math.round((deterministicJitter(date) + 0.5) * 10);
    // 일부 날짜는 0 (학습하지 않은 날)
    rows.push({ date, solvedCount: Math.max(0, level - 2) * 4 });
  }
  return rows;
}

// 2026년 1~6월 학생 학습 기록 (푼 문제 수)
const studyLog = buildStudyLog({ year: 2026, month: 1, day: 5 }, 180);

const CalendarHeatmapCustomStyleExample = () => (
    <CalendarHeatmap
      data={studyLog}
      dateKey="date"
      valueKey="solvedCount"
      valueLabel="학습 시간(분)"
      colorRange={[
        token('colors.blue.light.100'),
        token('colors.blue.light.400'),
      ]}
      weekdayLabels={['', '월', '', '수', '', '금', '']}
      monthLabelFormat={(d) => `${d.getFullYear()}.${pad(d.getMonth() + 1)}`}
      showLegend
      showTooltip
    />
  );

export default CalendarHeatmapCustomStyleExample;

전체 0 (학습 기록 없음)

import { CalendarHeatmap } from '@mildang/design-system/Chart';

function deterministicJitter(seed: string): number {
  let h = 1779033703 ^ seed.length;
  for (let i = 0; i < seed.length; i += 1) {
    h = Math.imul(h ^ seed.charCodeAt(i), 3432918353);
    h = (h << 13) | (h >>> 19);
  }
  h = Math.imul(h ^ (h >>> 16), 2246822507);
  h = Math.imul(h ^ (h >>> 13), 3266489909);
  return (h >>> 0) / 4294967296 - 0.5;
}

const pad = (n: number) => String(n).padStart(2, '0');

/** 시작일부터 days일간의 학습 기록 — deterministicJitter 기반 결정적 샘플 (Math.random 금지) */
function buildStudyLog(start: { year: number; month: number; day: number }, days: number) {
  const rows: { date: string; solvedCount: number }[] = [];
  for (let i = 0; i < days; i += 1) {
    const d = new Date(start.year, start.month - 1, start.day + i);
    const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
    const level = Math.round((deterministicJitter(date) + 0.5) * 10);
    // 일부 날짜는 0 (학습하지 않은 날)
    rows.push({ date, solvedCount: Math.max(0, level - 2) * 4 });
  }
  return rows;
}

// 2026년 1~6월 학생 학습 기록 (푼 문제 수)
const studyLog = buildStudyLog({ year: 2026, month: 1, day: 5 }, 180);

const CalendarHeatmapAllZeroExample = () => (
    <CalendarHeatmap
      data={studyLog.map((row) => ({ ...row, solvedCount: 0 }))}
      dateKey="date"
      valueKey="solvedCount"
      valueLabel="푼 문제 수"
      showLegend
    />
  );

export default CalendarHeatmapAllZeroExample;

반응형 (너비 자동 조정)

import { CalendarHeatmap } from '@mildang/design-system/Chart';

function deterministicJitter(seed: string): number {
  let h = 1779033703 ^ seed.length;
  for (let i = 0; i < seed.length; i += 1) {
    h = Math.imul(h ^ seed.charCodeAt(i), 3432918353);
    h = (h << 13) | (h >>> 19);
  }
  h = Math.imul(h ^ (h >>> 16), 2246822507);
  h = Math.imul(h ^ (h >>> 13), 3266489909);
  return (h >>> 0) / 4294967296 - 0.5;
}

const pad = (n: number) => String(n).padStart(2, '0');

/** 시작일부터 days일간의 학습 기록 — deterministicJitter 기반 결정적 샘플 (Math.random 금지) */
function buildStudyLog(start: { year: number; month: number; day: number }, days: number) {
  const rows: { date: string; solvedCount: number }[] = [];
  for (let i = 0; i < days; i += 1) {
    const d = new Date(start.year, start.month - 1, start.day + i);
    const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
    const level = Math.round((deterministicJitter(date) + 0.5) * 10);
    // 일부 날짜는 0 (학습하지 않은 날)
    rows.push({ date, solvedCount: Math.max(0, level - 2) * 4 });
  }
  return rows;
}

// 2026년 1~6월 학생 학습 기록 (푼 문제 수)
const studyLog = buildStudyLog({ year: 2026, month: 1, day: 5 }, 180);

const CalendarHeatmapResponsiveExample = () => (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 32, width: '100%' }}>
      {([320, 560, 760] as const).map((w) => (
        <div key={w} style={{ width: w, height: 160 }}>
          <CalendarHeatmap
            data={studyLog}
            dateKey="date"
            valueKey="solvedCount"
            valueLabel="푼 문제 수"
          />
        </div>
      ))}
    </div>
  );

export default CalendarHeatmapResponsiveExample;