VueでGitHubの草を実装してみた
はじめに
こんにちは、hiro です。
現在、個人開発でちょっとしたウェブアプリケーションを開発しています。このアプリケーションの機能の一つとして、コンテンツの投稿機能を実装しています。開発を進める中で、ユーザーが投稿した日付や数を可視化できれば、ユーザーのモチベーション向上につながるのではないかと考えました。
実際、私自身も GitHub の「草」のおかげで、毎日少しでも個人開発にコミットすることができています。
そこで今回、Cal-Heatmap
というライブラリを使用して、GitHub の「草」のような機能を実装した過程を、備忘録的に記事にまとめることにしました。
この記事が、同様の機能を実装したい方々の参考になれば幸いです。
デモ画像です。
私のプロジェクトがダークモード非対応なので白ベースですが頑張ればこんな感じになります。
ツールチップや、本家にはない今日の日付を強調することも出来ます。
選定理由
-
Vue
に特化したヒートマップ用ライブラリが適切なものがなかった- 存在はしているが、
Composition API
未対応だったり、メンテナンスが不活発だった
- 存在はしているが、
-
対照的に、
React
にはReact-heatmap
という整備されたライブラリが存在- サードパーティライブラリの充実度が
React
採用のメリットの一つだと再認識
- サードパーティライブラリの充実度が
-
JavaScript
(TypeScript
)で記述されており、特定のフレームワークに依存しない-
React
やVue
など、どのフレームワークでも使用可能。今後技術スタックを変更する際にもスムーズに移行できる
-
そのためこの記事はどちらかというとReact
ユーザーよりもVue
ユーザー向けになるかなと思いますが、このライブラリを使用する上で記述法などは大きく変わらないので問題はないかと思います。
実行環境(採用技術など)
ライブラリ名(バージョン)
- Vue.js(3.4.15)
プロジェクト自体は TypeScript,Composition API で記述していますが、本記事では便宜上、JavaScript を使用します。 - Cal-Heatmap(4.2.4)
本記事メインのライブラリです - Day.js(1.11.11)
日付操作のために使用しています。 - Tailwind CSS(3.4.1)
スタイリングのために使用しています。こちらも詳しい使用法などは解説しないのであらかじめご了承ください。
インストールとか
CDN と NPM に対応しています。(筆者は NPM でインストールしました)
CDN を使用する場合
<head>
タグ内に以下の記述をします
<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
そしてインポート
import CalHeatmap from 'cal-heatmap';
import 'cal-heatmap/cal-heatmap.css';
上記のようにエントリーポイントであるファイルに読み込ませるのが一般的かと思います。
私は以下のように、CalHeatmap
は使用するコンポーネントで、cal-heatmap/cal-heatmap.css
はエントリーポイントのファイルで読み込みました。
<script setup>
import CalHeatmap from 'cal-heatmap';
// その他の読み込み
</script>
import 'cal-heatmap/cal-heatmap.css';
基本的な使い方
公式には以下のように記載されています。
- Install the script
- Insert
<div id="cal-heatmap"></div>
where you want to render the calendar in your page- Instantiate and paint the calendar with your desired options/plugins
要は、
- まずインストールして、(済)
- カレンダー(ヒートマップ)を描画したいページに
<div id="cal-heatmap"></div>
を追加して、(未実施) - カレンダー(ヒートマップ)をインスタンス化し、希望のオプション/プラグインでペイントする。(未実施)
という 3 ステップのみで OK だよ、ということみたいです。
カレンダー(ヒートマップ)を描画したいページを用意する
公式にある通り<div id="cal-heatmap"></div>
という記述をカレンダーを描画したい部分に追加します。
id="cal-heatmap"
と指定されていますが、この値は後でセレクタとして使用するために指定されています。そのため、必ずしも cal-heatmap
である必要はなく、任意の値に変更しても構いません。
<script setup>
import CalHeatmap from 'cal-heatmap';
</script>
<template>
<div id="cal-heatmap"></div>
</template>
カレンダー(ヒートマップ)をインスタンス化し表示する
3 ステップ目です。
公式には以下のサンプルコードが記載されています。
const cal = new CalHeatmap();
cal.paint({});
render(<div id="cal-heatmap"></div>);
実行している内容は、CalHeatmap
クラスをインスタンス化し、paint
メソッドを使用してカレンダーを描画します。
私の場合は以下のように記述しました。
<script setup>
import { onMounted } from 'vue';
import CalHeatmap from 'cal-heatmap';
const cal = new CalHeatmap();
onMounted(() => cal.paint({}));
</script>
<template>
<div id="cal-heatmap"></div>
</template>
結果は以下になります。
CalHeatmap
クラスをインスタンス化し、paint
メソッドを使用するだけでカレンダーを作成することができましたね。
すごく簡単です。
しかしこのままでは、カレンダーが時間を基準に作成されている上に、UI も GitHub の草には程遠いのでオプションを駆使して修正していこうと思います。
実装したコードの全容
時間がない方はここだけでもご覧になっていって下さい。
export const heatmapData = [
{ date: '2024-08-01', value: 1 },
{ date: '2024-08-20', value: 4 }
];
<script setup>
import { onMounted } from 'vue';
import { heatmapData } from './data';
import CalHeatmap from 'cal-heatmap';
import Tooltip from 'cal-heatmap/plugins/Tooltip';
const cal = new CalHeatmap();
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
textAlign: 'start',
offset: {
x: 0,
y: 0
},
rotate: null
},
sort: 'asc'
};
const subDomain = {
type: 'ghDay',
gutter: 2,
width: 11,
height: 11,
radius: 2,
label: null
};
const date = {
start: new Date(),
highlight: [new Date()],
locale: 'ja',
timezone: 'Asia/Tokyo'
};
const data = {
source: heatmapData,
x: 'date',
y: (d) => +d['value'],
defaultValue: null
};
const scale = {
color: {
type: 'threshold',
range: ['#e6e6e6', '#4dd05a'],
domain: [0, 1]
}
};
const options = {
itemSelector: '#cal-heatmap',
domain,
subDomain,
date,
data,
scale
};
const TOOLTIP_OPTIONS = {
enabled: true,
text: (_, value, dayjsDate) => {
return `${value ?? 0} 件の投稿 ${dayjs(dayjsDate).format('YYYY/MM/DD')}`;
}
};
cal.paint(options, [[Tooltip, TOOLTIP_OPTIONS]]);
}
onMounted(() => paintCalendar());
</script>
<template>
<div id="cal-heatmap"></div>
</template>
Options
paint
メソッドの引数として渡すことのできるオプションは 11 種類あるようです。
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";
};
ただ、今回は GitHub 風のカレンダーヒートマップを実装することが目標なので、実際に使用したオプションのみ紹介・解説していきます。
また、オプションをpaint
メソッドの引数にどんどん追加していくのは冗長な記述になってしまうので、options
というオブジェクトを定義してその中に追加していきたいと思います。
それに伴って、現在は Vue のライフサイクルフックであるonMounted
のコールバック関数としてpaint
メソッドを呼び出していますが、そのコールバック関数の部分を一つの関数として定義したいと思います。
<script setup>
import { onMounted } from 'vue';
import CalHeatmap from 'cal-heatmap';
const cal = new CalHeatmap();
function paintCalendar() {
const options = {
// この中にオプションを追加していく
};
cal.paint(options);
}
onMounted(() => paintCalendar());
</script>
<template>
<div id="cal-heatmap"></div>
</template>
itemSelector
カレンダーを表示する場所を指定する:カレンダーがどこに描画されるかを指定するために使用されます。このオプションでは、カレンダーを挿入する HTML 要素を、要素そのものか W3C セレクタ文字列(例: #my-id
や .myclass
)として指定できます。
デフォルト値は#cal-heatmap
です。
そのため今までのサンプルコードでは指定していませんでしたが、カレンダーを表示することができていましたが、便宜上明示的に指定します。
function paintCalendar() {
const options = {
+ itemSelector: '#cal-heatmap'
};
cal.paint(options);
}
domain
カレンダーの時間単位を設定する:カレンダーの時間単位ごとのまとまり(画像の赤枠部分)を「ドメイン」と呼び、その設定をするために指定します。
domain
オプションの中でも細かくプロパティが分かれています。
type DomainOptions: {
type: string;
gutter: number;
padding: [number, number, number, number];
dynamicDimension: boolean;
label: LabelOptions;
sort: 'asc' | 'desc';
}
今回は以下のような指定を記述しました。
<script setup>
function paintCalendar() {
+ const domain = {
+ type: 'month',
+ gutter: 2,
+ padding: [0, 0, 0, 0],
+ label: {
+ text: 'M月',
+ position: 'top',
+ textAlign: 'start',
+ offset: {
+ x: 0,
+ y: 0
+ },
+ rotate: null,
+ },
+ sort: 'asc'
+ }
const options = {
itemSelector: '#cal-heatmap',
+ domain, // domain: domain と同義。変数名とプロパティ名が同じなのでこの記法。
};
cal.paint(options);
}
</script>
使用しているtype
gutter
padding
label
sort
についてそれぞれ簡単に解説していきます。
type
・時間単位を表すドメインの種類を設定します。
デフォルト値はhour
です。
利用可能なドメインの種類は 5 種類あり、文字列で指定します。
-
year
(年) -
month
(月) -
week
(週) -
day
(日) -
hour
(時間)
GitHub 風の草を実装するにはmonth
を選択します。
function paintCalendar() {
const domain = {
+ type: 'month',
};
const options = {
itemSelector: '#cal-heatmap',
domain
};
cal.paint(options);
}
gutter
・各ドメイン間のスペースを設定します。
単位はピクセルで、デフォルト値は4
です。
GitHub は月同士の間隔がかなり狭い印象なので、私は2
を指定しました。
function paintCalendar() {
const domain = {
type: 'month',
+ gutter: 2,
};
const options = {
itemSelector: '#cal-heatmap',
domain
};
cal.paint(options);
}
padding
・各ドメイン同士のパディングを配列で設定します。
デフォルト値は [0, 0, 0, 0]
です。
パディングは不要だと判断したので私はデフォルト値を指定しています。
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
+ padding: [0, 0, 0, 0],
};
const options = {
itemSelector: '#cal-heatmap',
domain
};
cal.paint(options);
}
label
・カレンダーのドメインに対してラベルを表示するための設定を行います。これにより、ラベルの内容や位置、テキストの整列方法、回転などを細かくカスタマイズできます。
label
はオブジェクトで、以下のようなプロパティを持っています。
type LabelOptions: {
text?: string | null | ((timestamp: number, element: SVGElement) => string);
position: 'top' | 'right' | 'bottom' | 'left',
textAlign: 'start' | 'middle' | 'end',
offset: {
x: number,
y: number,
},
rotate: null | 'left' | 'right',
width: number,
height: number,
}
・text
ラベルの内容を指定します。
デフォルト値はundefined
です。
値の型 | 説明 | 例の値 | 例の出力 |
---|---|---|---|
undefined | 選択されたタイプに基づいてカレンダーが自動的にラベルを決定 | なし | March(3 月) |
string |
dayjs のフォーマットを指定し、その結果を表示 |
MMMM | March(3 月) |
null | ラベルを表示しない | null | |
function | 関数の戻り値を表示。関数はドメインのタイムスタンプとラベルの SVG 要素を引数に取る | function (timestamp) { return new Date(date).toISOString(); } |
2022-12-06T20:01:51.290Z |
今回は画像のように「〇〇月」としたいのでtext: 'M月'
と指定します。
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
+ label: {
+ text: 'M月'
+ },
};
const options = {
itemSelector: '#cal-heatmap',
domain
};
cal.paint(options);
}
・position
ラベルの表示位置を指定します。ドメインに対して上下左右(top
bottom
left
right
)の 4 つの位置から選べます。
デフォルト値はbottom
です。
私はtop
を指定しています。
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
+ position: 'top',
},
};
const options = {
itemSelector: '#cal-heatmap',
domain
};
cal.paint(options);
}
・textAlign
ラベルの水平方向のテキスト整列方法を指定します。指定可能なのはstart
middle
end
の 3 種類で、デフォルト値はmiddle
です。
私はstart
を指定しています。
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
+ textAlign: 'start',
},
};
const options = {
itemSelector: '#cal-heatmap',
domain
};
cal.paint(options);
}
・offset
ラベルの x 軸と y 軸の位置をピクセル単位で微調整できます。
特に調整は必要ないので私はどちらも0
を指定しています。
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
textAlign: 'start',
+ offset: {
+ x: 0,
+ y: 0
+ },
},
};
const options = {
itemSelector: '#cal-heatmap',
domain
};
cal.paint(options);
}
・rotate
ラベルを回転させて縦書き表示にするオプションです。
-
null
: 回転しない(デフォルト) -
left
: 左に回転(縦書き) -
right
: 右に回転(縦書き)
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
textAlign: 'start',
offset: {
x: 0,
y: 0
},
},
+ rotate: null,
};
const options = {
itemSelector: '#cal-heatmap',
domain
};
cal.paint(options);
}
sort
・ドメインの並び順を昇順(asc
)降順(desc
)で設定します。
デフォルト値はasc
です。
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
textAlign: 'start',
offset: {
x: 0,
y: 0
},
},
rotate: null,
+ sort: asc
};
const options = {
itemSelector: '#cal-heatmap',
domain
};
cal.paint(options);
}
domain
内の子要素を設定する:subDomain
domain
内の子要素(画像赤枠)に関する設定ができます。
subDomain
オプションの中でも細かくプロパティが分かれています。
type 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);
}
前述しているdomain
と重複する部分も多数あるのが分かりますね。
重複している内容については解説を省略します。
今回subDomain
に関しては以下のように記述しました。
<script setup>
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
textAlign: 'start',
offset: {
x: 0,
y: 0
},
rotate: null,
},
sort: 'asc'
}
+ const subDomain = {
+ type: 'ghDay',
+ gutter: 2,
+ width: 11,
+ height: 11,
+ radius: 2,
+ label: null
+ };
const options = {
itemSelector: '#cal-heatmap',
domain,
+ subDomain, // subDomain: subDomain と同義。変数名とプロパティ名が同じなのでこの記法。
};
cal.paint(options);
}
</script>
type
とradius
について軽く解説します。
type
・domain
で使用しているtype
と同様の役割ですが、利用可能な種類が若干異なります。
month
week
day
hour
minute
xDay
ghDay
使用可能なsubDomain
は使用しているdomain
の種類に依存するので注意が必要です。
私はghDay
というsubDomain
を採用しました。
ghDay
は、各列が 1 週間を表すようにカレンダーが整列し、各ドメインの開始と終了が月の最初と最後の曜日で丸められます。この設定により、各列の中で欠けている日がないことが保証されます。
ちなみにghDay
はdomain
の種類がmonth
のみ許容します。
radius
・subDomain
のセルの border radius をピクセル単位で指定します。
数値が大きければ大きいほどセルが丸みを帯びます。
デフォルト値は0
です。
現段階での表示です。
いい感じに完成に近づいてきてます。
date
カレンダーの時間境界と設定を指定する:このオプションを使うことで、カレンダーがどの期間から開始するか、ナビゲーションの際の最小・最大日付の制限、特定の日付を強調表示するかどうか、タイムゾーンや言語の設定などを細かく制御できます。
date
オプションの中でも細かくプロパティが分かれています。
type DateOptions: {
start: Date;
min?: Date;
max?: Date;
highlight?: Date[];
locale: string | Partial<ILocale>;
timezone?: string
}
今回は、
- 今日の日付を強調したい
- タイムゾーンを日本時間にしたい
という要件を満たすためにhighlight
timezone
を指定しています。
<script setup>
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
textAlign: 'start',
offset: {
x: 0,
y: 0
},
rotate: null,
},
sort: 'asc'
}
const subDomain = {
type: 'ghDay',
gutter: 2,
width: 11,
height: 11,
radius: 2,
label: null
};
+ const date = {
+ start: new Date(),
+ highlight: [new Date()],
+ locale: 'ja',
+ timezone: 'Asia/Tokyo'
+ };
const options = {
itemSelector: '#cal-heatmap',
domain,
subDomain,
+ date // date: date と同義。変数名とプロパティ名が同じなのでこの記法。
};
cal.paint(options);
}
</script>
使用しているstart
highlight
locale
timezone
についてそれぞれ簡単に解説していきます。
start
・カレンダーの開始日を設定します。
デフォルト値はnew Date()
(今日の日付)です。
特定の日付を指定したい場合は、new Date('2024-01-01')
のように指定できます。
highlight
・強調したい日付の配列を指定します。 強調されたサブドメインセルは、目立つように特別なクラスが与えられます。
デフォルト値は[]
(空の配列)です。
強調したい日付を Date 型で配列の要素として指定すれば強調できます。
const cal = new CalHeatmap();
cal.paint({
date: {
highlight: [
new Date('2020-01-15'),
new Date(), // Highlight today
],
},
});
highlight
のおかげで今日の日付が強調されています。
locale
・カレンダーの言語や地域に合わせた日付フォーマットや機能を設定するために使用されます。このオプションにより、曜日の開始日(例: 月曜日や日曜日)など、地域ごとに異なる表示や動作を制御できます。
locale
は、カレンダーの基盤となる Day.js ライブラリを通じて動作し、週の開始日、月や曜日の名前、相対時間(例えば「1 日前」など)といったロケール固有の機能を設定します。
locale
に文字列を渡すことで、対応する言語にカレンダーの表示が切り替わります。例えば、en
(英語)やfr
(フランス語)など、Day.js ライブラリがサポートするロケールを指定することが可能です。
デフォルト値はen
英語です。
const cal = new CalHeatmap();
cal.paint({
date: { locale: 'fr' }, // フランス語に設定
});
対応している言語は以下のリポジトリで確認できます。
timezone
・カレンダーで表示される日付や時間に対して特定のタイムゾーンを設定するために使用されます。デフォルトでは、ユーザーのブラウザから推測されたタイムゾーンが自動的に適用されますが、特定のタイムゾーンを手動で設定することもできます。
const cal = new CalHeatmap();
cal.paint({
date: { timezone: 'Asia/Tokyo' },
});
使用可能なタイムゾーンは以下を参考にしてください。
data
カレンダーに表示するデータを取得し、処理する: カレンダーに表示するためのデータを取得したり、処理したりするために指定します。
type DataRecord = Record<string, string | number>;
type DataGroupType = 'sum' | 'count' | 'min' | 'max' | 'average';
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,
};
サンプルのデータを作成してエクスポートし、コンポーネントでインポートします。
export const heatmapData = [
{ date: '2024-08-01', value: 1 },
{ date: '2024-08-20', value: 4 }
];
<script setup>
+ import { heatmapData } from './data';
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
textAlign: 'start',
offset: {
x: 0,
y: 0
},
rotate: null,
},
sort: 'asc'
}
const subDomain = {
type: 'ghDay',
gutter: 2,
width: 11,
height: 11,
radius: 2,
label: null
};
const date = {
start: new Date(),
highlight: [new Date()],
locale: 'ja',
timezone: 'Asia/Tokyo'
};
+ const data = {
+ source: heatmapData,
+ x: 'date',
+ y: (d) => +d['value'],
+ defaultValue: null
+ };
const options = {
itemSelector: '#cal-heatmap',
domain,
subDomain,
date,
+ data // data: data と同義。変数名とプロパティ名が同じなのでこの記法。
};
cal.paint(options);
}
</script>
source
・使用するデータを文字列(URL など)または配列で指定します。
指定方法は上記のようにローカルのデータを参照する方法と、URL を指定してリモートリソースを参照する方法があります。
以下はリモートリソースを参照する場合の記述方法です。
const cal = new CalHeatmap();
cal.paint({
data: { source: 'https://your-api.com/data.json' },
});
x
・データセットの中から、どのプロパティが日付を表しているか、またはどのように日付を計算するかをカレンダーに伝えるためのオプションです。
文字列またはタイムスタンプを返す関数を指定することができます。
以下はx
を文字列で指定してあり、column1
というプロパティが日付を表しているよ、というのをカレンダーに伝えています。
const data = [{ column1: '2012-01-01', column2: 3 }];
cal.paint({
data: { source: data, x: 'column1' },
});
以下はx
にタイムスタンプを返す関数を指定してあり、column1: '2012-01-01'
を元にタイムスタンプを返しています。
const data = [{ column1: '2012-01-01', column2: 3 }];
cal.paint({
data: {
source: data,
x: datum => {
return +new Date(datum['column1']);
},
},
});
y
・データセットの中から、どのプロパティが値を表しているか、またはどのように値を計算するかをカレンダーに伝えるためのオプションです。値として数値または文字列を返すことができます。
- 文字列:オブジェクト内の値を持つプロパティ名を指定します。カレンダーは、そのプロパティから値を抽出します。
- 関数:データセットの各オブジェクト(datum)を引数として取り、そのオブジェクトから数値または文字列として返す関数を指定します。
以下はy
を文字列で指定してあり、column2
というプロパティに対応している3
を取得しています。
const data = [{ column1: '2012-01-01', column2: 3 }];
cal.paint({
data: { source: data, y: 'column2' },
});
以下はy
に関数を指定してあり、カレンダーには high と low の平均値(この場合、30 + 16 = 46 を 2 で割った 23)がその日付の値として表示されます。つまり、この処理では、複数の値を使って 1 つのデータポイントの値を計算し、その結果をカレンダーに反映しています。
const data = [{ date: '2012-01-01', high: '30', low: '16' }];
cal.paint({
data: {
source: data,
y: datum => {
return +datum['high'] + +datum['low']) / 2;
},
},
});
defaultValue
・データセットに日付の値がない場合のデフォルト値を数値または文字列で指定します。
デフォルト値はnull
です。
ここまでの記述で以下のようになっているかと思います。
若干分かりづらいですが、data
内の日付に対応するセルの色が変わっているのが分かります。
scale
カレンダーの色を設定する: いよいよ最終局面です。
このオプションを使うことでカレンダーに色をつけることができ、データのあるセルは色がつくようになります。
任意の色を指定することができるので、GitHub 風に緑色になるように指定していきます。
type scaleOptions = {
opacity?: {
baseColor: string,
domain: number[],
type: string,
},
color?: {
scheme: string,
range: string[] | d3-scale-chromatic,
interpolate: string | d3-interpolator,
domain: number[],
type: string,
},
};
今回私は以下のような指定をしました。
<script setup>
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
textAlign: 'start',
offset: {
x: 0,
y: 0
},
rotate: null,
},
sort: 'asc'
}
const subDomain = {
type: 'ghDay',
gutter: 2,
width: 11,
height: 11,
radius: 2,
label: null
};
const date = {
start: new Date(),
highlight: [new Date()],
locale: 'ja',
timezone: 'Asia/Tokyo'
};
const data = {
source: heatmapData,
x: 'date',
y: (d) => +d['value'],
defaultValue: null
};
+ const scale = {
+ color: {
+ range: ['#e6e6e6', '#4dd05a'],
+ domain: [0, 1],
+ type: 'threshold',
+ }
+ };
const options = {
itemSelector: '#cal-heatmap',
domain,
subDomain,
date,
data,
+ scale // scale: scale と同義。変数名とプロパティ名が同じなのでこの記法。
};
cal.paint(options);
}
</script>
color
・2 色以上の色を使ってデータセットを表現します。
種類によって、連続的な色の範囲、または離散的な色のセットを使用します。
range
・独自のカラーパレットを定義するために、色の配列、または d3 配色関数を指定します。
-
配列を指定する場合
色の配列を使用する場合、最低 2 色が必要です。 認識される色名は、色の名称(red)、16 進カラーコード(#ff0000
)、rgb(rgb(0, 0, 255)
)など、CSS が受け付ける任意の値です。sample.jsconst cal = new CalHeatmap(); cal.paint({ scale: { color: { range: ['red', '#0000FF'], }, }, });
-
d3 の配色関数を指定する場合
以下に定義されているものを使用することができます。
https://d3js.org/d3-scale-chromatic/diverging#schemeSpectralsample.jsconst cal = new CalHeatmap(); cal.paint({ scale: { color: { range: d3.schemeSpectral[3], }, }, });
今回はデータが 1 件もなければ白、1 件以上あれば緑色、というような仕様にしたいので以下を指定しました。
scale: {
color: {
range: ['#e6e6e6', '#4dd05a'],
}
}
domain
・少なくとも 2 つの値からなる配列で、データセットの最小値と最大値を指定します。
後に説明するtype
でthreshold
の型を使用する場合、異なる threshold
のリストでなければなりません。
type
・どのように色を割り当てるかを制御することができます。
指定可能なオプションは全部で 17 種類あり、例えばlinear
を指定すると色の変化がデータ値に対して線形に行われ、データの最小値から最大値までが連続して変化するようなものに向いています。温度データを 0 度から 30 度まで連続的に変化する色で表現する場合などにぴったりです。
対してordinal
を指定すると離散的なカテゴリーデータに対して色を割り当てることができ、各月ごとに異なる色を割り当てて表示するなど順序がないデータに最適です。
今回は指定された閾値に基づいて、データを複数の範囲に分割し、各範囲に異なる色を割り当てたいのでthreshold
を指定しました。
threshold
を使用することで閾値によって適用したい色を変更できるので便利です。
分かりづらいと思うので、以下のコードで解説します。
color: {
type: 'threshold',
range: ['#e6e6e6', '#4dd05a', '', 'red'],
domain: [10, 20, 30],
},
上記の例では
-
range
に#e6e6e6
(白)、#4dd05a
(緑)、''
(空なのでデフォルト)、red
(赤)を指定し、 -
domain
に[10, 20, 30]
を指定しています。
これはデータの数値が
- 0 ~ 10 の範囲 =>
#e6e6e6
- 11 ~ 20 の範囲 =>
#4dd05a
- 21 ~ 30 の範囲 =>
' '
デフォルトの色 - 31 より大きい 範囲 =>
red
のように範囲ごとに色を変えることができます。
無事に色が変わりましたね 👏
9 割完成ですが最後の仕上げでツールチップも実装します。
必要ない方はここで読み終えていただいても大丈夫です!
お疲れ様でした!
Plugins
ツールチップを使用するためにはプラグインを使用する必要があります。
インストール
CDN を使用する場合
<script src="https://unpkg.com/@popperjs/core@2"></script>
<script src="https://unpkg.com/cal-heatmap/dist/plugins/Tooltip.min.js"></script>
NPM を使用する場合
このプラグインは CalHeatmap のコアに組み込まれているのでプラグインを使用したいコンポーネントで import するだけで使用できます。
import Tooltip from 'cal-heatmap/plugins/Tooltip';
使い方
使い方も非常に簡単で、paint
メソッドの第二引数に配列として渡すだけで使用できます。
const cal = new CalHeatmap();
cal.paint({}, [[Tooltip, TOOLTIP_OPTIONS]]);
ツールチップ実装後のコード
<script setup>
+ import Tooltip from 'cal-heatmap/plugins/Tooltip';
function paintCalendar() {
const domain = {
type: 'month',
gutter: 2,
padding: [0, 0, 0, 0],
label: {
text: 'M月',
position: 'top',
textAlign: 'start',
offset: {
x: 0,
y: 0
},
rotate: null,
},
sort: 'asc'
}
const subDomain = {
type: 'ghDay',
gutter: 2,
width: 11,
height: 11,
radius: 2,
label: null
};
const date = {
start: new Date(),
highlight: [new Date()],
locale: 'ja',
timezone: 'Asia/Tokyo'
};
const data = {
source: heatmapData,
x: 'date',
y: (d) => +d['value'],
defaultValue: null
};
const scale = {
color: {
range: ['#e6e6e6', '#4dd05a'],
domain: [1],
type: 'threshold',
}
};
const options = {
itemSelector: '#cal-heatmap',
domain,
subDomain,
date,
data,
scale
};
+ const TOOLTIP_OPTIONS: TooltipOptions = {
+ enabled: true,
+ text: (_, value, dayjsDate) => {
+ return `${value ?? 0} 件の投稿 ${dayjs(dayjsDate).format('YYYY/MM/DD')}`;
+ }
+ };
+ cal.paint(options, [[Tooltip, TOOLTIP_OPTIONS]]);
}
</script>
TooltipOptions
ツールチップのオプションを設定できます。
interface TooltipOptions extends PluginOptions, PopperOptions {
enabled: boolean;
text: (timestamp: number, value: number, dayjsDate: dayjs.Dayjs) => string;
}
enabled
・ツールチップを有効にするかどうかを設定でき、デフォルト値はtrue
です。
ツールチップの UI をカスタマイズするには、CSS で#ch-tooltip
を探します。
text
・以下 3 つの引数を受け取り、ツールチップの内容を返す関数を指定します。
-
timestamp
: 現在のサブドメインのタイムスタンプ。単位はミリ秒。 -
value
: 現在のサブドメインの値。 -
dayjsDate
: ロケール/タイムゾーンを意識した dayjs オブジェクトで、日付の操作と書式設定を容易にするために提供される。
今回は以下のような記述を行いました。
text: (_, value, dayjsDate) => {
return `${value ?? 0} 件の投稿 ${dayjs(dayjsDate).format("YYYY/MM/DD")}`;
};
データがなければ「0 件の投稿 ○○○○/○○/○○」
データがあれば 「○ 件の投稿 ○○○○/○○/○○」
というツールチップが表示されるようになります。
ちなみに第一引数の_
はこの引数は使用しないけど、構文上必要なので置いておくような場合に使われます。
これも tips として覚えておきましょう。
本当にお疲れ様でした!
ツールチップまで表示できていれば完成です。
最後に
ここまで読んでくださってありがとうございました。
自分でもびっくりしておりますが、ここまで長い記事になるとは思っていませんでした。
ただ、記事のボリュームと裏腹に、実装のハードルはそこまで高くないです。
Cal-heatmap かなりいいのでぜひ個人開発やプロダクトに採用してみてください!
おまけ
色々な実装例が showcase に記載されているのでぜひ参考にされて下さい。
Discussion