一人もくもく会の自分の投稿が増えてきたので、GitHubの草を生やすやつを作ってみると気分がいいのではないかと思い作成してみた。
↓こういうの
Vueで。
本家はsvgで描画しているらしい。とりあえず適当にテーブルで作った。
<template>
<div class="panel panel-default">
<div class="panel-body">
<table>
<tr v-for="(row, index) in contributionRows" :key="index">
<th v-if="index === 0"></th>
<th v-if="weekdayName(index)" rowspan="2">{{weekdayName(index)}}</th>
<td
v-for="(cell, cellIndex) in row"
:key="cellIndex"
v-bind:class="cell.getClass()"
data-toggle="tooltip"
data-animation="false"
data-container="body"
data-placement="top"
data-html="true"
title=""
:data-original-title="cell.getMessage()"
></td>
</tr>
</table>
<div class="note">
<span class="string">Less</span>
<span class="color bg-grey-200"></span>
<span class="color bg-green-200"></span>
<span class="color bg-green-400"></span>
<span class="color bg-green-600"></span>
<span class="color bg-green-800"></span>
<span class="color bg-green-900"></span>
<span class="string">More</span>
</div>
</div>
</div>
</template>
<style scoped>
div.panel {
margin-top: 40px;
}
table {
border: 0;
border-collapse: separate;
border-spacing: 2px;
}
th {
font-weight: lighter;
font-size: 10px;
transform: scale(0.9);
transform-origin: 0 0;
padding-right: 0.5rem;
vertical-align: top;
}
td {
width: 10px;
height: 10px;
}
.note {
margin-top: 1rem;
text-align: right;
}
.note span.string {
margin-left: 1rem;
margin-right: 1rem;
font-size: 9px;
}
.note span.color {
display: inline-block;
width: 10px;
height: 10px;
}
</style>
<script>
import moment from 'moment'
class Cell {
constructor(date, count) {
this.date = date.clone();
this.count = count;
}
getClass() {
if (this.count === 0) {
return 'bg-grey-200';
} else if (this.count <= 2) {
return 'bg-green-200';
} else if (this.count <= 4) {
return 'bg-green-400';
} else if (this.count <= 6) {
return 'bg-green-600';
} else if (this.count <= 8) {
return 'bg-green-800';
}
return 'bg-green-900';
}
getMessage() {
return '<b>' + this.getCountMessage() + '</b> on ' + this.date.format('YYYY-MM-DD');
}
getCountMessage() {
if (this.count === 0) {
return 'No contributions';
}
return `${this.count} contributions`;
}
}
export default {
props: ['contributions'],
data() {
let contributionRows = [];
for (let i = 0; i < 7; i++) {
contributionRows[i] = [];
}
const today = moment().format('YYYY-MM-DD');
const baseDate = moment().add(-364, 'days');
const startDate = baseDate.add(-baseDate.weekday(), 'days');
for (let date = startDate.clone(); date.format('YYYY-MM-DD') <= today; date = date.add(1, 'days')) {
contributionRows[date.weekday()].push(new Cell(date, this.getCountForDate(date)));
}
return {contributionRows}
},
mounted() {
$('[data-toggle="tooltip"]').tooltip();
},
methods: {
getCountForDate(date) {
const formatDate = date.format('YYYY-MM-DD');
if (this.contributions[formatDate] !== undefined) {
return this.contributions[formatDate];
}
return 0;
},
weekdayName(index) {
if (index == 1) {
return 'Mon';
} else if (index == 3) {
return 'Wed';
} else if (index == 5) {
return 'Fri';
}
}
}
}
</script>
tooltip有効化のためだけにVueコンポーネントにするのはなんとなく微妙ではある…。
呼び出し。
<contribution-map :contributions="<?= h(json_encode($contributionMap)) ?>"></contribution-map>
集計処理。
Cake\Chronos\Dateで時間を考慮しない日付を扱える。時間が関わってくると意外に厄介だったりするので。(これくらいでは関係ないが)
UsersTable.php
public function createContributionMap($id)
{
$end = Date::today();
$start = Date::today()->subYear(1)->addDay(1);
$start = $start->subDay($start->dayOfWeek);
$summary = $this->Meetings->getDateSummary($id, $start, $end);
return $summary;
}
実際の集計。Meetingをクローズする際に必ずMeetingPostが追加されるので、MeetingPostがあればその数、なければ1を加えていく。
-
$postCount
は通常のfind。CakePHP3はQueryになるので、そのまま集計クエリのサブクエリとして使うことができる。 -
$date
はMySQLのDATE_FORMATの処理。マニュアルにはちゃんとした説明がなかったのだが使ってみたら使えた。
MeetingsTable.php
public function getDateSummary($userId, Date $start, Date $end)
{
$query = $this->find();
$postCount = $this->MeetingPosts->find()
->select([$query->func()->count('*')])
->where([
'MeetingPosts.meeting_id = Meetings.id',
'MeetingPosts.deleted' => false,
]);
$date = $query->func()->date_format([
'created' => 'identifier',
"'%Y-%m-%d'" => 'literal'
]);
$summary = $query->select([
'date' => $date,
'post_count' => $postCount,
])
->where([
'Meetings.user_id' => $userId,
'Meetings.deleted' => false,
function ($exp, $q) use ($date, $start, $end) {
return $exp->between($date, $start, $end);
}
])
->all();
$result = [];
foreach ($summary as $row) {
if (!isset($result[$row->date])) {
$result[$row->date] = 0;
}
$result[$row->date] += $row->post_count ?: 1;
}
return $result;
}
できた。
日付処理がPHPとJSで2重になっているので集計処理の際にテーブル型の配列にしてしまっても良いかもしれない。