学園祭サイトになくてはならないタイムテーブルのつくりかた

# はじめに
約2年半、早稲田理工の学園祭『**理工展**』のWebサイトを作り、この記事を書いた昨日に最後に活動を終えました。
HTMLすらろくに書けませんでしたが、今ではサイトを1から自分で作れるようになりました！  

そんな環境下で、個人的にハマった**タイムテーブル**の一作成方法を紹介したいと思います。

# 開発環境
## 言語
- PHP (7.3.10)
- SCSS

## その他フレームワーク等
特になし

# 学園祭特有のタイムテーブルとは
学園祭となると、参加団体のステージパフォーマンスや教授の講演といったものが数多く行われます。
来場者の方に存分に学園祭を楽しんでいただけるよう、**一目見てわかりやすいタイムテーブルを提供することが重要**です。
出演時刻を文字だけで伝えるのではなくテーブル形式で出力することで、同時刻で行われている団体が何であるのか瞬間的に目に訴えることができます。

# 完成形
自分が担当したタイムテーブルは昨年のものなので、今年のではありませんが以下のリンクにあるようなものを作成しました。
団体が出演する時間に合わせて、タイムテーブル中にある黄色いボックスの位置と高さが調整されているのがわかると思います。

[https://archive.rikoten.com/php/pages/events/timetable.php](https://archive.rikoten.com/php/pages/events/timetable.php)

(レイアウトは異なりますが、今年のタイムテーブルは[こちら](https://www.rikoten.com/php/pages/events/timetable.php)です)

# 実装
本記事では、ステージが2つある学園祭を想定して実装の手順を説明していきます。
完成目標は以下の画像のようなもので、上で紹介した完成形と同じようなものになっています。

![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488154/008c3e6c-964c-4b39-bc05-4b37282fa886.png)

## データ構造
2つのステージ各々について`$data`配列に情報を入れます。
格納する情報としては、**開始時刻**, **終了時刻**, **関連リンク**, **団体名**です。
時刻に関しては、abcd分を`abcd`と繋げて入力します(10時30分は1030)。

(各ステージの第一要素は**ステージ名**と**クラス名**を入れる)

```php
/* data.php */
$data = [
    [
        ["ステージ1","stage1"],
        /* [開始時刻, 終了時刻, 関連リンク, 団体名] */
        [1000, 1030, "#", "オープニング"],
        [1040, 1110, "#", "ダンスサークルA"],
        [1130, 1225, "#", "よさこいサークル"],
        [1300, 1340, "#", "男子チアサークル"],
        [1400, 1425, "#", "有名人トークショー"],
        [1450, 1530, "#", "ダンスサークルB"],
        [1550, 1630, "#", "マジックサークル"],
        [1640, 1700, "#", "閉会式"],
    ],
    [
        ["ステージ2","stage2"],
        [1010, 1050, "#", "マジックサークル"],
        [1120, 1210, "#", "男子チアサークル"],
        [1240, 1320, "#", "マジックサークル"],
        [1400, 1500, "#", "よさこいサークル"],
        [1520, 1600, "#", "ダンスサークルA"],
    ],
];
```

## 出力
ここでは全てのコードを提示していますが、重要なところを詳細に説明しています。
全体像を把握するぐらいの気持ちでご覧ください。
(CSSは別途下で掲示しています)

```php

<!-- timetable.php -->

<html>
<head>
	<meta charset="utf-8">
	<title>タイムテーブルサンプル</title>
	<link rel="stylesheet" type="text/css" href="./style.css">
</head>

<body>
<?php
require_once("./data.php");

//学園祭開始時刻(理工展は10時)を分単位に直した値
define('tableTopMinutes', '600');

// タイムテーブルの中のリンク付きボックスを出力する関数
function echoEventBox($start, $end, $link, $html=""){
	// ここからが核になる部分
    $time = sprintf("%d:%02d〜%d:%02d", (int)($start/100), $start%100, (int)($end/100), $end%100);

    // 開始時刻と終了時刻を分単位へ計算(10:00 -> 600px, 12:30 -> 750px等)し、学園祭開始時刻(理工展では10時)を引く
    $start = (int)($start/100)*60 + $start % 100 - tableTopMinutes;
    $end = (int)($end/100)*60 + $end % 100 - tableTopMinutes;

    // 1h(60px)を200pxにするために$rateをかける
    $rate = (double)10 / 3;

    // タイムテーブルの上部からの距離
    $top = $start * $rate;

    // リンク付ボックスの高さ 
    $height = ($end - $start) * $rate;
	
	// 関連リンクの有無で場合分け
	if($link == NULL){
		print<<<EOT
		<a class="eventBox" style="height: {$height}px; top: {$top}px; text-decoration: none;">
EOT;
	}
	else{
		print<<<EOT
		<a href="{$link}" class="eventBox" style="height: {$height}px; top: {$top}px; text-decoration: none; word-break: break-word;">
EOT;
	}
	print<<<EOT
		<button class="eventBtn"">
			<span>{$time}</span>
			<span>{$html}</span>
		</button>
	</a>
EOT;
}

// タイムテーブルを出力するメインとなる関数
function echoEventTimeTable($tableData){
	
	print<<<EOT
	<table class="table">
		<col span="1" class="time">
EOT;
	print("<thead><tr>");
	print('<th class="time" style="width: 30px;">時刻</th>');
	for($i = 0; $i < count($tableData); $i++){
		echo "<th class = '{$tableData[$i][0][1]}'>{$tableData[$i][0][0]}</th>";
	} 
	print<<<EOT
			</tr>
		</thead>
		<tbody>
			<tr>
				<td class='time'>10</td>
EOT;
	for($i=0; $i<count($tableData); $i++){
		print<<<EOT
		<td class="{$tableData[$i][0][1]}" rowspan="8">
EOT;
		// 各ステージごとの企画数だけeventBoxを出力
		$rows = $tableData[$i];
		for($k=1; $k<count($rows); $k++){
			$row = $rows[$k];
			// echoEventBox(開始時刻, 終了時刻, 関連リンク, 団体名);
			echoEventBox($row[0], $row[1], $row[2], $row[3]);
		}
		print('</td>');
	}
	print("</tr>");

	// 時間の表示
	for($k=11; $k<17; $k++){
		print("<tr><td class='time'>{$k}</td></tr>"); 
	}
		print<<<EOT
			</tbody>
		</table>
	</div>
EOT;
}

?>
	
	<h2 style="text-align: center;">タイムテーブル</h2>
	<hr>
	<?php echoEventTimeTable($data); ?>

</body>
</html>
```

# 詳細説明(echoEventBox関数)
リンク付ボックスを出力する関数(echoEventBox関数)です。
以下の部分が**この記事の核**となります。

```php
// ここからが核になる部分
$time = sprintf("%d:%02d〜%d:%02d", (int)($start/100), $start%100, (int)($end/100), $end%100);

// 開始時刻と終了時刻を分単位へ計算(10:00 -> 600px, 12:30 -> 750px等)し、学園祭開始時刻(理工展では10時)を引く
$start = (int)($start/100)*60 + $start % 100 - tableTopMinutes;
$end = (int)($end/100)*60 + $end % 100 - tableTopMinutes;

// 1h(60px)を200pxにするために$rateをかける
$rate = (double)10 / 3;

// タイムテーブルの上部からの距離
$top = $start * $rate;

// リンク付ボックスの高さ 
$height = ($end - $start) * $rate;
```

後に紹介するCSS(SCSS)にて、**1時間あたりの高さを200px**と定義していますので、PHPのほうでもこれに合わせる必要があります。
まず、`$data`配列に格納した開始時刻と終了時刻の取り扱いです。

「10時は0時から600分後の時刻」という事実を使って各団体の開始時刻と終了時刻をpxに変換します。
そのための変換式が

```php
(int)($start/100)*60 + $start % 100
```

これです。ただ、学園祭の開始時刻以前の情報はいらないのでその分の時間の差をとります。それが`tableTopMinutes`です。
これはechoEventBox関数のすぐ上に

```php
//学園祭開始時刻(理工展の場合は10時)を分単位に直した値
define('tableTopMinutes', '600');
```

と定義しています(10時開始の場合は600px)。
終了時刻も同様です。

しかし、これでは1時間あたりのpx数は60pxであり、200pxではありません。
なので、`$rate`を用いて60pxを200pxに変換します。

最後にボックスの開始位置と高さですが、ボックスは`posision: absolute;`を適用させているので、その`top`を開始時刻に対応するpx数に設定します。
また、高さは終了時刻から開始時刻を引いた値で良いでしょう。

上の説明を図にしたのがこれです。

<img width="631" alt="exp.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488154/1e27d69e-9cbe-8475-12f2-6eb12b99eb0a.png">

# CSS(SCSS)
重要な部分はここです

```scss
td{
    @extend tr;
	text-align:center;
	padding: 0px !important;
    position: relative;
    height: 200px; // 1時間あたりの高さ
}
```

**tdの高さ=1時間あたりの高さ**としています。

```scss
// style.scss

@charset "utf-8";

$point: 768px;
// スマホ対応
@mixin mobile{
  @media (max-width: ($point)){
    @content;
  }
}
// パソコン対応
@mixin pc{
    @media (min-width: ($point + 1px)){
      @content;
    }
}

*{
	@include mobile{
		font-size: 16px;
	}
	@include pc{
		font-size: 24px;
	}
}

a{
    text-decoration: none;
    &:hover{
        text-decoration: none;
    }
}

// デフォルトのbutton設定が嫌なので基本値に戻す。以下のコートを引用しました。
// https://qiita.com/nabettu/items/1593af04e48444c45c53
button{
	background-color: transparent;
	border: none;
	cursor: pointer;
	outline: none;
	padding: 0;
	appearance: none;
}

table{
	table-layout: fixed;
	width: 100%;
}
tr{
    border: 2px solid #FFC7AF;
}
td{
    @extend tr;
	text-align:center;
	padding: 0px !important;
    position: relative;
    height: 200px; // 1時間あたりの高さ
}
th {
    @extend tr;
	background-color: white;
	color: black;
	text-align:center;
}

.eventBtn{
    display: block;
    background-color: rgba(255, 214, 0, 0.65);
    width: 100%;
	height: 100%;
	border-radius: 5px;
    &:hover{
        background-color: rgba(255, 214, 0, 0.38);
	}
	span{
		&:first-child{
			position: absolute;
			top: 0;
			left: 0;
			display: block;
			margin: 0px;
			padding: 0px 8px 0px;
			color: black;
		}
	}
}

.eventBox{
	margin: 0px;
	position: absolute;
	width: 100%;
	white-space: normal;
	color: black;
	display: flex;
	align-items: center;
	justify-content: center;
}
.time{
	font-size: .7em;
	vertical-align: top;
}
```

# 改良の余地
かなり回りくどいことをしている自覚はあります。筆者が思いついただけ挙げていきます。

- 1時間あたりのpx数をCSSでなくPHP側で制御したい
- カラムが3つ以上の際、スマホで見ると横にはみ出るので、Ajax等を使ってユーザーが見たいステージのカラムのみ選択し表示させる
- データベースを使う場合にはまた別の対応が必要になる(理工展は昨年はデータベースを導入していませんでした)

# まとめ
- 各団体の出演時間は異なるので、それに合わせた**高さ設定**が必要。
- 高さ調整には**出演時刻をpxといった長さに変換**すると扱い易い

最後になりましたが、ここまで読んでいただきありがとうございます。
個人的に3記事目になるのですが、プログラムの説明って丁寧な説明を心がけても、どうも上手くできません...。申し訳ないです。

もしご不明な点や改善点等ございましたら編集リクエストを送っていただけると幸いです！
確認し次第対処いたします！