自分のブログのRecent post欄に、面白い工夫をしてみたいと思い実装してみました。
Table of Contents
中国兄貴たちの技術ブログがかっこいい!
と思った正月でした。多くのエンジニアが使っていたBlog技術要素がHexoだったのですが、そのなかでもひときわかっこいいデザインのThemeを使っている兄貴が多数おりました。
そのThemeこそ、hexo-theme-materyです。
Theme Demoページはこちら
Themeのなかで特にかっこいいなぁと思ったのは、Blogの投稿日、投稿数に応じてGitHubの草(Heatmap)を表現するところです。
Demoページは全然草生えてませんが・・・。
自分のブログにも導入したい!!ということで早速作ってみることにします。
ReactでGitHub Heat mapを作る
このブログはGatsby.jsというフレームワークで実装しているので要素技術はReactなのでReactでGitHubのHeat mapを作る方法を探していきます。
見つけました。
Kevin QiさんがReact Calendar Heatmapというものを提供してくれていました。
さっそく使っていきます。
Component作成
まずは何はともあれインストール。
npm install --save react-calendar-heatmap
使い方はとっても簡単でreact-calendar-heatmap#usageに載っているとおり、
import CalendarHeatmap from 'react-calendar-heatmap';
import 'react-calendar-heatmap/dist/styles.css';
<CalendarHeatmap
startDate={new Date('2016-01-01')}
endDate={new Date('2016-04-01')}
values={[
{ date: '2016-01-01', count: 12 },
{ date: '2016-01-22', count: 122 },
{ date: '2016-01-30', count: 38 },
// ...and so on
]}
/>
という具合で、propsにstartDate, endDate, valuesを指定すれば最低限OK。
今回はValuesをGatsby.jsのGraphQLに対応させたいので、React Componentを作っていきます。
Componentでは、StaticQueryを使って、全記事の日付を取得し、日付ごとにカウントを取りカウントをreact-calendar-heatmapに渡してあげることまでが役割とします。
サイドバーで使う場合、1年分の長さのheatmapだと長すぎるのでpropsで長さを直近から5ヶ月までと1年分と切り替えられるようにしたいと思います。
ということで実装したものが下記です。
import CalendarHeatmap from 'react-calendar-heatmap';
import 'react-calendar-heatmap/dist/styles.css';
import React from "react";
import ReactTooltip from 'react-tooltip';
import {graphql, StaticQuery} from "gatsby";
import {gotoPage} from '../../api/url';
const getLastYearDate = () => {
const today = new Date();
today.setFullYear( today.getFullYear() - 1 );
return today
};
const getLast5MonthDate = () => {
const today = new Date();
today.setMonth( today.getMonth() - 5 );
return today
};
const getSlug = (value) => {
if (!value || !value.date) {
return null;
}
const {slug} = value;
gotoPage(slug);
};
const getTooltipDataAttrs = (value) => {
if (!value || !value.date) {
return null;
}
if (value.count === 1) {
return {
'data-tip': `${value.date} has ${value.count} post`,
};
} else {
return {
'data-tip': `${value.date} has ${value.count} posts`,
};
}
};
const Heatmap = ({data, minify=false}) => {
const {allMarkdownRemark} = data;
const mapping = {};
const slugs = {};
const values = [];
let startDate;
if (minify) {
startDate = getLast5MonthDate()
} else {
startDate = getLastYearDate()
}
allMarkdownRemark.edges.forEach(({node}) => {
const {date, slug} = node.frontmatter;
if (mapping[date]) {
mapping[date] += 1;
} else {
mapping[date] = 1;
}
slugs[date] = slug;
});
Object.keys(mapping).forEach( (date) => {
values.push({date: date, count: mapping[date], slug: slugs[date]})
});
return (
<>
<CalendarHeatmap
startDate={startDate}
endDate={new Date()}
values={values}
showMonthLabels={true}
showWeekdayLabels={true}
onClick={getSlug}
tooltipDataAttrs={getTooltipDataAttrs}
/>
<ReactTooltip />
</>)
};
export default props => (
<StaticQuery
query={graphql`
query {
allMarkdownRemark {
edges {
node {
frontmatter {
date(formatString: "YYYY-MM-DD")
slug
title
}
}
}
}
}
`}
render={data => <Heatmap data={data} {...props} />}
/>
)
要素ごとにお話すると、Gatsby.js templateでGraphQLを実行するわけではないので、GraphQLはStaticQueryを使わないといけません。下記のようなクエリにすれば全記事の日付が取得できます。
query {
allMarkdownRemark {
edges {
node {
frontmatter {
date(formatString: "YYYY-MM-DD")
slug
title
}
}
}
}
}
`}
簡単ですね。さらにGraphQLのデータを渡し、renderしたものを別componentに渡すために、
export default props => (
<StaticQuery
query={graphql`
query {
allMarkdownRemark {
edges {
node {
frontmatter {
date(formatString: "YYYY-MM-DD")
slug
title
}
}
}
}
}
`}
render={data => <Heatmap data={data} {...props} />}
/>
)
としてやっているのです。
肝心なreact-carender-heatmap部分はL47〜ですが、基本的にはGraphQLから取得した日付のカウントを取りつつvaluesに渡しているだけです。
heatmapクリック時には該当記事へジャンプする機能とTooltipを表示させる機能を実現するためにCalendarHeatmapにonClickとtooltipDataAttrs propsを設定し、関数を設定してあげています。
また、tooltip利用にはReactTooltipを用意してあげる必要があります。
Reactではcomponentが複数要素を返すことが基本的にはできないので、React.fragmentを使ってCalendarHeatmapとReactTooltipの2要素をreturnしてあげます。
<> </>はfragmentの短縮形です。
return (
<>
<CalendarHeatmap
startDate={startDate}
endDate={new Date()}
values={values}
showMonthLabels={true}
showWeekdayLabels={true}
onClick={getSlug}
tooltipDataAttrs={getTooltipDataAttrs}
/>
<ReactTooltip />
</>)
またComponentのpropsを使ってstartDateはサイドバー用は5ヶ月前、ページに埋め込む用は1年と変更できるようにしています。
残念ながらゴミコードです。
let startDate;
if (minify) {
startDate = getLast5MonthDate()
} else {
startDate = getLastYearDate()
}
導入してみた
作ったComponentをサイドバーに導入してみました。
お、なかなかいい感じ。
結論
今年もかっこいいWeb勉強していきたいですね。よろしくお願いします。