uiw/react-heat-map + GitHub APIを用いてReactでGitHubの草 (Contribution) を作ってみよう

## 概要
ReactでGitHubの草(Contribution)を再現してみる記事になります。

![スクリーンショット 2024-12-21 13.34.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689205/e3dbee78-2a03-2157-c75c-0d1148807565.png)


## 使用するライブラリ

[@uiw/react-heat-map](https://www.npmjs.com/package/@uiw/react-heat-map) を使います。

https://github.com/uiwjs/react-heat-map

:::note info
他にもReactでヒートマップを作成できるライブラリはあるのですが、2024年12月現在、定期的に更新されているライブラリとしてこちらを選定しました。
:::

## 実装

```Heatmap.tsx
import { ContributionValue, GitHubContributionWeeks } from "@/types/contribution";
import HeatMap from '@uiw/react-heat-map';
import { useEffect, useState } from "react";

export const Heatmap = () => {

  const nowDate = new Date();
  const heatmapStartDate = nowDate.setMonth(nowDate.getMonth() - 4);
  const [heatmapValue, setHeatmapValue] = useState<ContributionValue[]>([{date: heatmapStartDate.toString(), count: 0}]);


  // GitHub APIに送信する情報
  const USERNAME = process.env.GITHUB_USERNAME;
  const headers = {
    'Authorization': `bearer ${process.env.GITHUB_TOKEN}`,
  }
  const body = {
    "query": `query {
        user(login: "${USERNAME}"){
          contributionsCollection {
            contributionCalendar {
              totalContributions
              weeks {
                contributionDays {
                  contributionCount
                  date
                }
              }
            }
          }
        }
      }`
  }
  
  const generateContributionValues = (weeks: GitHubContributionWeeks) => {
    const contributionValues: ContributionValue[] = [];

    for (const week of weeks) {
      week.contributionDays.map((obj) => {
        contributionValues.push({count: obj.contributionCount, date: obj.date})
      })
    }

    return contributionValues;
  }

  useEffect(() => {
    // コンポーネント読み込み時にGitHubからContributionの情報を取得する
    const fetchData = async () => {
      const response = await fetch('https://api.github.com/graphql', 
        { 
          method: 'POST', 
          body: JSON.stringify(body), 
          headers: headers 
        });
      return response;
    };

    (async() => {
      const response = await fetchData();
      const data = await response.json();
      const weeks = data.data.user.contributionsCollection.contributionCalendar.weeks;
      const contributionValues = generateContributionValues(weeks);
      setHeatmapValue(contributionValues);
    })();
  }, []);


  return (
    <div className="heatmap">
      <HeatMap
        value={heatmapValue} 
        startDate={new Date(heatmapStartDate)}
        endDate={new Date()}
        panelColors={{
          1: '#161b22',
          2: '#0e4429',
          3: '#006d32',
          5: '#26a641',
          8: '#39d353'
        }}
      />
    </div>
  );
};


```

### 自身のGitHubからContribution情報をとってくる

.envファイルに自身のGitHubの認証情報を記載し、GitHub GraphQL APIでContribution情報を取得します。

```tsx
const USERNAME = process.env.GITHUB_USERNAME;
const headers = {
  'Authorization': `bearer ${process.env.GITHUB_TOKEN}`,
}
const body = {
  "query": `query {
      user(login: "${USERNAME}"){
        contributionsCollection {
          contributionCalendar {
            totalContributions
            weeks {
              contributionDays {
                contributionCount
                date
              }
            }
          }
        }
      }
    }`
}
```

コンポーネント読み込み時にGitHub GraphQL APIを呼び出します。
```tsx
  useEffect(() => {
    // コンポーネント読み込み時にGitHubからContributionの情報を取得する
    const fetchData = async () => {
      const response = await fetch('https://api.github.com/graphql', 
        { 
          method: 'POST', 
          body: JSON.stringify(body), 
          headers: headers 
        });
      return response;
    };

    (async() => {
      const response = await fetchData();
      const data = await response.json();
      const weeks = data.data.user.contributionsCollection.contributionCalendar.weeks;
      const contributionValues = generateContributionValues(weeks);
      setHeatmapValue(contributionValues);
    })();
  }, []);
```

APIのレスポンスからContributionの情報をヒートマップで利用できるように成型してあげます。


```tsx

  const generateContributionValues = (weeks: GitHubContributionWeeks) => {
    const contributionValues: ContributionValue[] = [];

    for (const week of weeks) {
      week.contributionDays.map((obj) => {
        contributionValues.push({count: obj.contributionCount, date: obj.date})
      })
    }

    return contributionValues;
  }

  /* 以下のようなオブジェクトの配列に成型する
    [
        {
            count: 0
            date: "2024-12-01"
        },
        {
            count: 1
            date: "2024-12-02"
        },
        ...
    ]
  */
```

`heatmapValue`に成型したデータをsetします。
```tsx
setHeatmapValue(contributionValues);
```


### ヒートマップ

`panelColors`プロパティで、Contributionの数でパネルの色を変化させることができます。

```tsx
  return (
    <div className="heatmap">
      <HeatMap
        value={heatmapValue} // GitHubから取得したContributionデータ(成型済)をvalueとして持たせる
        startDate={new Date(heatmapStartDate)}
        endDate={new Date()}
        panelColors={{
          1: '#161b22',
          2: '#0e4429',
          3: '#006d32',
          5: '#26a641',
          8: '#39d353'
        }}
      />
    </div>
  );

```


GitHubのContributionを再現することができました!

![スクリーンショット 2024-12-18 7.05.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689205/326dbf48-3579-83bd-04a4-c6f2509b5aac.png)


## おまけ: Tooltipの実装

react-tooltip を活用すれば、ホバー時にContribution数を表示させることができます。

![スクリーンショット 2024-12-21 13.44.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689205/58c83603-1ef7-cb59-9f28-5996c08b5679.png)


```tsx
  import { Tooltip } from 'react-tooltip';
  
  return (
    <div className={styles["activity"]}>
      <HeatMap
        value={heatmapValue} 
        startDate={new Date(heatmapStartDate)}
        endDate={new Date()}
        panelColors={{
          1: '#161b22',
          2: '#0e4429',
          3: '#006d32',
          5: '#26a641',
          8: '#39d353'
        }}
        rectRender={(props, data) => {
          if (!data.count) return <rect {...props} />;
          return (
            <>
              <rect data-tooltip-id={`heatmap`} onMouseEnter={() => setTooltipData({...data})} {...props} />
            </>
          );
        }}
      />
      <Tooltip id={`heatmap`} content={`count: ${tooltipData.count} date: ${tooltipData.date}`}/>
    </div>
  );

```