GASでスプレッドシートにレーダーチャートを作成

# 1. 今回やりたいこと・構成図と流れ
Googleフォームで集計したアンケートを点数化し、動的に結果を取得してレーダーチャートを作成します。

![構成図](https://gyazo.com/9033e64107bf936b4b50168e95aa8035.jpg)

①Google formで集計したアンケートをGoogle sheetに保存
②Google Sheetの結果をGASで解析してデータ取得
③データを元に、HTMLでレーダーチャートの描画
④描画したチャートを画像化
⑤画像をGoogle sheetに挿入
⑥画像をアンケートに答えたユーザーへメール送信

本記事では③〜⑥にかけて説明します。

# 2. Debug方法
今回はGASでgsファイルとhtmlファイルの両方を使用します。挙動するタイミングとファイルによって、出力方法が異なります。

-  「実行ボタン」を押してからhtmlファイルが呼ばれるまでは`console.log()`で「実行ログ」に出力されます
- htmlファイルのスクリプトタグ内では`console.log()`で、GoogleChromeだとディベロッパーツールのコンソールに出力されます
- htmlファイル呼び出し後のgsファイルでは`Logger.log()`で、GASの実行数の項目で確認可能です。ここでは、正しい挙動ができていない場合に、関数が実行されているかどうかの確認もできます。

    ![実行数の画面](https://i.gyazo.com/de22db1cd41c900869980c076e9ab673.jpg)

# 3. レーダーチャートの描画
Google Chartsのリファレンスに記載のある`VegaChart`を使って、レーダーチャートを描画します。下記URLにアクセスし、コードをコピーしてください。

https://developers.google.com/chart/interactive/docs/gallery/vegachart_examples?hl=ja#radar-chart-example

![VegaChart](https://i.gyazo.com/4c440e843cee707dedf71172ff1c36b1.png)

GASに戻り任意のhtmlファイルを作成します。本記事では`create_chart.html`としています。
そこに先ほどコピーしたコードを貼り付けてください。

次は.gsファイルを作成し、実際にレーダーチャートを描画していきます。

### ウェブサイトに表示
1. 下記コードを任意の.gsファイルへ記載します
    ```javascript
    function doGet(){
        return HtmlService.createHtmlOutputFromFile('create_chart');
    }
    ```
    <br>
1. ウェブアプリとしてデプロイし、取得したURLを開けばレーダーチャートが描画されます
1. `doGet`メソッドは、このあと使用しないので、消去もしくはコメントアウトしておいてください

（↓`doGet`メソッドについて丁寧にまとめてくれている記事を共有します）

https://tonari-it.com/gas-web-doget-html-output/

### Google Sheetのモーダルダイアログに表示
これは、上記のウェブサイトに表示する方法とは異なり、Google Sheet上にレーダーチャートを表示させることができます。
さらにデプロイをする必要がなく、GASで「実行ボタン」を押下した後に、対象のGoogle Sheetを開くことで表示が可能です。
つまり、動的に値を取得してレーダーチャートを表示させるのに適した方法だと言えます。

:::note warn
注意
「実行ボタン」を押下してから2,3秒以内にGoogle Sheetを開かないと、モーダルダイアログは表示されません。
:::

1. 下記コードを任意の.gsファイルへ記載します。本記事では`main.gs`としています。
    ##### main.gs
    ```javascript
		function createChartInSpreadsheet() {
			var html = HtmlService.createHtmlOutputFromFile('create_chart')
				.setWidth(400)
				.setHeight(1000);
		
			SpreadsheetApp.getUi().showModalDialog(html, 'ダイアログの題名');
		}
    ```
    <br>
1. 「実行ボタン」を押し、対象のGoogle Sheetを開くことで、レーダーチャートが描画されます。今回は幅400px, 高さ1000pxで設定しています。

（↓ダイアログを表示させるメソッドについて丁寧にまとめてくれている記事を共有します）

https://tonari-it.com/gas-html-show-dialog/#toc3

# 4. 動的なデータでチャートを作成
これまではリファレンスのコードをコピー＆ペーストし、固定のデータを使用していました。ここでは、動的にデータを取得し、それに応じたレーダーチャートが表示されるようにします。

### .gsファイルからデータを取得

現状はhtmlファイル内でデータを宣言していますが、アンケート結果を点数化したものを.gsファイルから取得したいです。

`create_chart.html`を書き換えます。定数で記載されているデータを消去し、下記コードへ変更します。

##### create_chart.html
```javascript
	function loadCharts() {
	  google.script.run.withSuccessHandler(function(result) {
	    lasagna = result.lasagna;
	    pork = result.pork;
	    melon = result.melon		
	    addChart(lasagna[0][2], lasagna, "#B82E2E", 1);
	    addChart(pork[0][2], pork, "#6633CC", 10);
	    addChart(melon[0][2], melon, "#109618", 20);
	  }).getData();
	}
```

#### コード解説
- `google.script.run` ：.gsファイルにある関数の呼び出し
- `.getData()` ：.gsファイルにある関数。後述します。
- `.withSuccessHandler(function(result))` :`getData()`関数の実行が成功したら`()`内が挙動します。`result`引数の中に`getData()`関数の戻り値が格納されています。

次は`main.gs`に下記コードを追加します。
この状態で実行しても、Google sheetのモーダルダイアログにレーダーチャートが正しく表示されているはずです。

##### main.gs
```javascript
    function getData() {	
		const lasagna = [
			["Protein",0.1308,"Lasagna, cheese, frozen, prepared"],
			["Carbohydrates",0.05032727272727273,"Lasagna, cheese, frozen, prepared"],
			["Vitamin C",0.228,"Lasagna, cheese, frozen, prepared"],
			["Calcium",0.08538461538461538,"Lasagna, cheese, frozen, prepared"],
			["Zinc",0.11375,"Lasagna, cheese, frozen, prepared"],
			["Sodium",0.18933333333333333,"Lasagna, cheese, frozen, prepared"]
		  ];
		
		  const pork = [
			["Protein",0.2638,"Pulled pork in barbecue sauce"],
			["Carbohydrates",0.06814545454545454,"Pulled pork in barbecue sauce"],
			["Vitamin C",0.002666666666666667,"Pulled pork in barbecue sauce"],
			["Calcium",0.033846153846153845,"Pulled pork in barbecue sauce"],
			["Zinc",0.23125,"Pulled pork in barbecue sauce"],
			["Sodium",0.444,"Pulled pork in barbecue sauce"]
		  ];
	
		  const melon = [
			["Protein",0.0168,"Melons, cantaloupe, raw"],
			["Carbohydrates",0.029672727272727274,"Melons, cantaloupe, raw"],
			["Vitamin C",0.4893333333333334,"Melons, cantaloupe, raw"],
			["Calcium",0.006923076923076923,"Melons, cantaloupe, raw"],
			["Zinc",0.0225,"Melons, cantaloupe, raw"],
			["Sodium",0.010666666666666666,"Melons, cantaloupe, raw"]
		  ];
	
		return { lasagna, pork, melon };
	}
```
    
### 動的にデータを取得する
そのために、GASのプロパティサービスを利用します。

https://developers.google.com/apps-script/guides/properties?hl=ja

これにより
>  **1つのドキュメントをスコープ**とする Key-Value ペアで単純な **データを保存**

することができます。

つまり、htmlファイルを呼び出す前に生成したデータを、保持しておくことが可能です。
またリファレンスに記載があるように、データ形式は文字列となりますので、格納および取得の際には注意が必要です。

##### main.gs
```javascript
	function main(){
        // データの形式は必要に応じて変更してください。
		let data = calData();
		PropertiesService.getScriptProperties().setProperty('result', JSON.stringify(data));
		createChartInSpreadsheet();
	}
	
	function createChartInSpreadsheet() {
		var html = HtmlService.createHtmlOutputFromFile('create_chart')
			.setWidth(400)
			.setHeight(1000);
	
		SpreadsheetApp.getUi().showModalDialog(html, 'ダイアログの題名');
	}
	
	function getData(){
		let data = PropertiesService.getScriptProperties().getProperty('result');
		
		// プロパティサービスの使用上dataは文字列です。一度ログに出力してから、データ管理を行うことを推奨します。
		Logger.log(data);
		
		// 以下省略
	}
```

### 余談：　データ取得の失敗例
手取り早く先に進みたい方は読み飛ばしていただいて結構です。続きは[こちら](#-5.-レーダーチャートの設定)から移動できます。

 #### NG: Google Sheetから取得

当初は、.gsファイルでデータを処理した際に、Google Sheetへデータを格納し、.htmlファイルを呼び出した後に、Google Sheetにあるデータを取得しようとしていました。

今回のプログラムは.gsファイルと.htmlファイルを行き来します。
その中で、.htmlファイルを呼び出した後にGoogle Sheetから値を取得しようとすると、挙動速度の影響か、データを取得する前にレーダーチャートを生成しようとしてしまっていました。

そのため、`getData()`関数において`Logger.log`で`data`変数を参照しても空の状態であり、モーダルダイアログにもチャートは描画されませんでした。

#### NG: グローバル変数の利用
.gsファイルで`data`変数をグローバル変数として管理する手法を考えました。しかし、html呼び出し前に生成したデータを、html呼び出し後に参照しても、空のままでした。下記サイトにて学びましたが、GASにはjavascriptのようなグローバル変数の使い方ができないとのことです。

https://teratail.com/questions/221926

では本題に戻ります。

# 5. レーダーチャートの設定

`Vega Chart`の`options`の設定をします。

- レーダーチャートで扱う数値の幅を設定
    - `scales`の2つ目の要素内にある`domain`の数値を変更することで設定が可能です
    ```json
    	"scales": [
		{
		  "name": "angular",
		  "type": "point",
		  "range": {"signal": "[-PI, PI]"},
		  "padding": 0.5,
		  "domain": {"data": "table", "field": "key"}
		},
		{
		  "name": "radial",
		  "type": "linear",
		  "range": {"signal": "[0, radius]"},
		  "zero": true,
		  "nice": false,
		  "domain": [0,0.5], // この箇所の右側の数値を変更
		}
	  ],
    ```  
<br>

- レーダーチャートを45度回転させる設定
    - 上記にある`scales`の`range`を変更します
    - 変更前： `"range": {"signal": "[-PI, PI]"},`
    - 変更後： `"range": {"signal": "[-PI/4, PI*7/4]"},`

<br>

- 表記される数値の設定
    - `marks`の2つ目の要素にある`enter`の`text`を変更することで設定可能です
    - 本記事では`%`とという単位と小数点表記をやめたかったため下記のように変更しました
        - 変更前：　`"text": {"signal": "format(datum.datum.value,'.1%')"}`
        - 変更後： `"text": {"signal": "format(datum.datum.value, '.0f')"}`
    ```json
        {
			"type": "text",
			"name": "value-text",
			"from": {"data": "category-line"},
			"encode": {
			  "enter": {
				"x": {"signal": "datum.x + 14 * cos(scale('angular', datum.datum.key))"},
				"y": {"signal": "datum.y + 14 * sin(scale('angular', datum.datum.key))"},
				"text": {"signal": "format(datum.datum.value,'.1%')"}, // ここで設定可能です
				"opacity": {"signal": "datum.datum.value > 0.01 ? 1 : 0"},
				"align": {"value": "center"},
				"baseline": {"value": "middle"},
				"fontWeight": {"value": "bold"},
				"fill": {"value": color},
			  }
			}
		}
    ```
<br>

- レーダーチャートと数値を近づける設定
    - 上記`encode`の`enter`にある`x`, `y`を変更します
    - 変更前： `"x": {"signal": "datum.x + 14 * cos(scale('angular', datum.datum.key))"`
    - 変更後： `"x": {"signal": "datum.x + 7 * cos(scale('angular', datum.datum.key))"`
    - `y`も同様に変更してください

# 6. レーダーチャートの画像化
いままではレーダーチャートをhtml上に描画していましたが、そこから画像へと変換し、Google Sheetへの挿入やメールへの添付を行っていきます。

Google Chartsのリファレンスに`getImageURI()`メソッドが用意されているとあります。

https://developers.google.com/chart/interactive/docs/gallery/vegachart?hl=ja#methods

しかし実際のページを確認すると、`CoreChart`と`GeoChart`のみ対応しているとのことです。

https://developers.google.com/chart/interactive/docs/printing?hl=ja

VegaChartは`getImageURI()`メソッドを使用して画像の取得ができないため、下記方法で画像化します。

#### 1. `loadCharts()`および`addChart()`関数の変更

##### create_chart.html
```javascript
	function loadCharts() {
		google.script.run.withSuccessHandler(function(result) {
		  test_A = result.test_A;
		  test_B = result.test_B;
		  test_C = result.test_C;
		
		  addChart(test_A[0][2], test_A, "#B82E2E");
		  addChart(test_B[0][2], test_B, "#6633CC");
		  let chart = addChart(test_C[0][2], test_C, "#109618");
		  google.visualization.events.addListener(chart, 'ready', captureAndSendImage);
		}).getData();
	}

    function addChart(title, data, color){
        // 省略
        return charts; // 追記
    }
```

`addChart()`関数で作成した`chart`変数の情報を`loadCharts()`関数へ戻し、canvasを使って画像化する`captureAndSendImage()`関数を呼び出しています。
その際、`google.visualization.events.addListener(chart, 'ready', captureAndSendImage);`という呼び出し方法をとっています。

チャートの描画は非同期で行われるため、描画の完了を待たずに次の処理に進むと、期待通りに動作しないことがあります。
イベントリスナーおよびreadyイベントを使用することで、描画完了後に次の処理を確実に実行できます。

https://developers.google.com/chart/interactive/docs/events?hl=ja

#### 2. 画像化する関数を追記

htmlの要素を画像化できる`html2canvas`というライブラリを使用します。

https://html2canvas.hertzen.com/documentation

下記では、画像をURLへ変換、整理し`insertChartToSpreadsheet`関数へ渡しています。

##### create_chart.html
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script>
 	function captureAndSendImage() {
		const chartDiv = document.getElementById('chart-area');
		
		const scale = 1.0;
		html2canvas(chartDiv, {
		  scale: scale,
		  useCORS: true,
		}).then(canvas => {
		  const base64Image = canvas.toDataURL().split(',')[1];
		  google.script.run
			.withSuccessHandler(() => google.script.host.close())
			.insertChartToSpreadsheet(base64Image);
		});
	}
</script>
```

# 7.　Google Sheetへの挿入とメール送信

画像の作成ができたので、Google Sheetへの挿入およびメールの送信を行います。下記を.gsファイルへ追記します。

##### main.gs
```javascript
	function insertChartToSpreadsheet(base64Image) {
		var blob = Utilities.newBlob(Utilities.base64Decode(base64Image), 'image/png', 'chart.png');
		
		var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
		sheet.insertImage(blob, 6, 1);
		sendMail_withImage(blob)
	  }
	  
	function sendMail_withImage(blob) {
		let sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
		let address = sheet.getRange(2, 4).getValue();	// Google Sheetの任意の場所から取得
		let title = "タイトル";
		let text = "本文。<img src='cid:inlineImg'>";
		let options = {
		  "htmlBody":text,
		  "inlineImages":
			{
			  inlineImg:blob
			}
		};
		 MailApp.sendEmail(address, title, text, options);
	}
```

https://qiita.com/a_eau_/items/01bc14bfc5cb5ef12e64

https://auto-worker.com/blog/?p=2827

これで「実行ボタン」を押すと、レーダーチャートを画像化し、Google Sheetへ挿入されるはずです。

# 8. おわりに
今回、記事にまとめたのも、レーダーチャートの使用に関して有用な記事があまりなく、開発にかなりの時間がかかりました。試行した例を下記に示します。おそらくこれらは実装ができません。(できる方いたら教えてほしいです)

- `Vegachart`を.gsファイルでの使用を試みたが困難（外部ライブラリの使用はhtmlファイルでないと不可能）
- トリガーを起点に挙動する場合、htmlファイルで実行した値を取得するなど動的な挙動が困難

最終的に「実行ボタン」を押したら、ループ文を使用して、複数のデータ処理ができるようになったのでよかったです。
GASのプロパティサービスに大助かりしました。

読者の方のお力になれれば幸いです。ありがとうございました。