JSでgithubの草を作る（Cal-Heatmap）

# Cal-Heatmap
githubの草（ヒートマップカレンダーと言います）のようなグラフをwebアプリで実装しようと調べまくってた時に、結構良いライブラリを発見したので、書いていきたいと思います。
jsonのようなデータを入れるだけで、カレンダーを一発で生成でき、かつ細かい設定もいじることができる優れものです。cssの羅列を書くよりはよっぽどマシだと思いました。

https://cal-heatmap.com/

めっちゃがんばると、こんなのができたりします。そっくりですね
![スクリーンショット 2023-09-09 10.17.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/323e9774-d1df-028d-01e9-b19ce767a55a.png)

# インストールとか
CDNとNPMに対応しています。

### CDN
```html:index.html
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://unpkg.com/cal-heatmap/dist/cal-heatmap.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/cal-heatmap/dist/cal-heatmap.css">
```
### NPM
```:
$npm install cal-heatmap
```
```javascript:main.js
import CalHeatmap from 'cal-heatmap';
import 'cal-heatmap/cal-heatmap.css';
```


# カレンダーのペイント
HTMLにレンダリングする場合、カレンダーの入る`div`タグを入れておきます。
後にセレクトするため、`id`やら`class`やらを指定しておくとよさそうです。
```html:index.html
<div id="cal-heatmap"></div>
```
HTML側での操作はこれで以上なので、ここからはJSだけで作っていきます。
まず、`CalHeatmap`クラスを呼び出し、`paint`メソッドでインスタンスを構築していきます。
```javascript:main.js
const cal = new CalHeatmap();
cal.paint();
```

# Options
`paint`メソッドでは、引数の`Options`オブジェクトによってカレンダーをカスタマイズする役割があります。今回は重要なところ以外は省かせていただきます。

```js:Type Signature
type Options = {
  itemSelector: Element | string,
  range: number,
  domain: DomainOptions,
  subDomain: SubDomainOptions,
  verticalOrientation: boolean,
  date: DateOptions,
  data: DataOptions,
  label: LabelOptions,
  animationDuration: number,
  scale: ScaleOptions,
  theme: 'light' | 'dark',
}
```

## `itemSelector`
ここでレンダリングするタグをセレクトします。DOMかW3Cセレクターに対応しています。
```javascript:main.js
cal.paint({ itemSelector: '#cal-heatmap' });

// または
cal.paint({ itemSelector: document.getElementById('cal-heatmap') });
```


## `domain`
カレンダーのドメイン（画像の赤枠）の要素やフォーマットを設定できます。
![スクリーンショット 2023-09-09 13.06.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/eee12ec1-0eed-8c0f-e42f-11d7baf2dc1e.png)

```js:main.js
cal.paint({ 
    domain:{
        type: string,
        gutter: number,
        padding: [number, number, number, number],
        dynamicDimension: boolean,
        label: LabelOptions,
        sort: 'asc' | 'desc',
    }
});
```

* `type`: ドメイン１つ分の期間（`default: 'hour'`）
* `gutter`: ドメイン同士の間隔px（`default: 4`）
* `padding`: ドメインのパディング（`default: [0,0,0,0]`）
* `label`: ドメインのラベル（画像の赤枠）を設定できます。
![スクリーンショット 2023-09-09 13.30.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/6b953bee-9788-3a03-a872-e19b79ac1e72.png)

* `sort` : ドメインの順序。`asc`で昇順、`desc`で降順（default : `asc`）

## `subDomain`
ドメイン内の子要素（画像の赤枠）の要素やフォーマットを設定できます。
![スクリーンショット 2023-09-09 13.45.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/4f933f63-18ca-d877-5641-9a466983dbfc.png)

```js:main.js
cal.paint({ 
    subdomain:{
        type: string,
        gutter: number,
        width: number,
        height: number,
        radius: number,
        label:
        | string
        | null
        | ((timestamp: number, value: number, element: SVGElement) => string),
        color:
        | string
        | ((timestamp: number, value: number, backgroundColor: string) => string),
    }
});
```
* `type`: 子要素の期間を指定します。親のドメインで設定した`type`よりも小さい単位を設定する必要があります。（例：`Domain: year => subDomain: month`）
* `gutter`: 子要素同士の間隔px（`default: 2`）
* `width`: 子要素の横幅（`default: 10`）
* `height`: 子要素の縦幅（`default: 10`）
* `radius`: 子要素の角の丸み（`default: 0`）
* `label`: 子要素の中にラベルを入れられます。
![スクリーンショット 2023-09-09 14.44.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/af057f6a-91fb-324a-f937-c5560a7d861a.png)

* `color`: 子要素の`label`の文字色（`default : black`）
* `sort` : 子要素の順序。`asc`で昇順、`desc`で降順（`default : asc`）

## `date`
カレンダーに表示する時間の範囲を設定できます。
```js:main.js
cal.paint({ 
    date:{
        start: Date,
        min: Date,
        max: Date,
        highlight: Date[],
        locale: string | Partial<ILocale>,
        timezone: string
    }
});
```

* `start`: カレンダーの開始日（`default: new Date()`）
* `highlight`: 指定された子要素に目立つようなデザインが施される。
* `locale`: 週の最初の日が月曜日か日曜日か。
* `timezone`: 日付のタイムゾーン

## `data`
カレンダーに使用するデータを設定できます。
```js:Type Signature
type DataOptions = {
  source: string | DataRecord[],
  type: 'json' | 'csv' | 'tsv' | 'txt',
  requestInit: object,
  x: string | ((datum: DataRecord) => number),
  y: string | ((datum: DataRecord) => number),
  groupY:
    | DataGroupType
    | ((values: (number | string | null)[]) => number | string | null),
  defaultValue: null | number | string,
};
```

* `source`: 使用するデータを指定します。指定の仕方は２つあります。
#### ローカル
プログラム内でjsonデータを作成し、引数として挿入。
```js:main.js
const data = [
    { date: '2012-01-01', value: 3 },
    { date: '2012-01-02', value: 6 },
];

const cal = new CalHeatmap();
cal.paint({
    data: {
        source: data
    },
});
```

#### リモート
外部ファイル、もしくは外部APIから取得したデータを入れることもできます。
```js:main.js
const cal = new CalHeatmap();
cal.paint({
    data: { 
        source: 'https://your-api.com/data.json' 
    }
});

```

* `type`: データのタイプを指定します。。json以外のデータタイプにも適応可能。（`default: json`）
* `x`: データのタイムスタンプとなるカラムのプロパティ名
* `y`: データのカウントとなるカラムのプロパティ名

#### xとyって？
例えば以下のようなデータを使う場合、

```js:main.js
const data = [{ column1: '2012-01-01', column2: 3 }];

cal.paint({
    data: { 
        source: data, 
        x: 'column1' ,
        y: 'column2'
    }
});
```
として挿入できる。jsonのプロパティ名は自由に設定できるのだ。


## `scale`
ヒートマップでの色の付け方を設定できます。
```js.main.js
cal.paint({
    scale:{
        opacity: {
            baseColor: string,
            domain: number[],
            type: string
        },
            color: {
            scheme: string,
            range: string[] | d3-scale-chromatic,
            interpolate: string | d3-interpolator,
            domain: number[],
            type: string
        }
    }
})
```
### `color`
* `scheme`: 既存で用意されているカラーパレットが使用される。
* `range`: オリジナルのカラーパレットを指定できる。２色以上を指定する。
* `interpolate`: `range`と組み合わせて使用する。色の補間範囲を定義する。
* `domain`: データセットの`y`のプロパティに入る数値の最大値と最小値。`type: threshold`を使用する場合は、しきい値になる。
* `type`: 配色に使用される値のタイプを指定する。
    * `linear(default)` - 線形変換 (平行移動とスケール)
    * `threshold` - `domain`で設定されたしきい値によって配色される
    * `sqrt` - 平方根変換
などなど

githubの草を再現するなら、`type: threshold`を使用します。

# 作ってみた
なんだかんだあって、データをランダムで生成させて作ってみました。
![スクリーンショット 2023-09-09 17.05.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092163/e8e1e5fd-5719-8645-78e4-3268db600968.png)

```js:main.js
const cal = new CalHeatmap();

cal.paint(
    {
        itemSelector: document.getElementById('cal-heatmap'),
        //or itemSelector: document.getElementById('cal-heatmap'),

        domain: {
            type: 'month',
            gutter: 5,
            label: {
                text: 'MMM', 
                textAlign: 'start', 
                position: 'top' 
            }
        },
        subDomain: {
            type:'ghDay',
            gutter:5,
            width:17,
            height:17,
            radius:3,
            label:null
        },
        date: {
            start: new Date('2023-01-01')
        },
        data: {
            source: 'data.json',
            x: 'date',
            y: 'count'
        },
        scale: {
            color: {
                type: 'threshold',
                range: ['#b0f5e5', '#35f2c6', '#0fbdb4', '#077485'],
                domain: [4, 6, 8]
            }
        }
    }
);
```
うまいことできてますね。
自動生成のプログラムも用意しましたのでよければ使ってみてください。

https://github.com/pdmuds4/cal-heatmap-test

# おわりに
他にもポップアウトを表示させたりイベントをつけたりすることができたりします。今回ご紹介しましたのは一部ですがこんな感じになります。ドキュメントにはshowcaseなども用意されているので、コピペして改良してもよいかもしれません。

https://cal-heatmap.com/docs/showcase