JavaScriptでグラフを作る

この記事は[ハンズラボ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/handslab) 16日目の記事です。

# 何をするのか
このようなイメージのページを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335368/01794a29-e153-b357-6b43-9c34c7ea1f7b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335368/c080f53e-27b9-fa46-cde4-fc46a801d2d3.png)

今回の記事の題材にしたのは家計簿です。
１ヶ月間の支払いを10項目に分けて集計したグラフなどを、ページに載せました。

# どうやるのか
Chart.jsを使います。

https://www.chartjs.org/

HTMLで、idを付けたcanvas要素を配置します。
JavaScriptで、このcanvas要素をグラフにするための、様々な設定をします。

# グラフの実装手順

## ライブラリ読み込み
#### npmの場合
```bash
npm install chart.js
```

```js
const Chart = require('chart.js');
```

この記事ではCDNで読み込みます。

https://www.chartjs.org/docs/latest/getting-started/installation.html#cdn

## HTMLで記述すること

1. canvas要素

    ```html
    <canvas id="chart-monthly-overview"></canvas>
    ```

    idを指定してください。ここのidをJavaScriptの方で使います。  

2. canvasの親要素
   canvas要素をdivタグなどで囲った親要素がある場合、divタグでstyle指定をすることでグラフの大きさや位置を調整することができます。しかし、グラフのオプション設定を合わせて行うこともあります。 
   この記事では、
   ① 親要素なし
   ② 親要素あり・グラフの大きさはJavaScriptで指定
   ③ 親要素あり・グラフの大きさはその親要素のstyleで指定
   の3パターンを実装しています。
3. 読み込むJavaScriptファイルの指定。あるいはスクリプト埋め込み。

## JavaScriptで記述すること① グラフの種類
折れ線グラフ、棒グラフ、レーダーチャート、円グラフ、散布図など描画できます。
チュートリアルの[Home > Chart Types](https://www.chartjs.org/docs/latest/charts/line.html)や[Samples](
https://www.chartjs.org/docs/latest/samples/bar/vertical.html)を見ると、描画できるもののイメージが掴めるでしょう。

この記事では、

- ドーナツグラフ
- 水平棒グラフ
- レーダーチャート
- 垂直棒・折れ線の混合グラフ

の4種類を描画します。

## JavaScriptで記述すること② グラフのラベル
グラフのデータ項目ひとつひとつの名前です。文字列の配列形式で設定します。

## JavaScriptで記述すること③ グラフのデータ
数値の配列形式です。ラベルの配列の要素数とデータの要素数が一致するようにします。

積み上げ棒グラフなど、一つの図に複数のグラフ項目を載せる場合は、それぞれのグラフ項目名とデータを連想配列の形で記します。

## JavaScriptで記述すること④ グラフの書式
グラフの見た目全般に対していろいろな設定ができます。例えば...

- グラフの線の色
- 塗りつぶしの色や透過度
- 凡例の位置や大きさ、凡例項目の並び
- 重なっているグラフ項目の前面・背面の順位
- 軸の最小値、最大値、フォント
- グラフタイトルやそのフォント
- 描画後に実行する関数
- グラフの縦・横の比率についての指定

など。

この記事では、これら設定をいくつか行います。
その中でも特に応用的な設定を実装したところを2つ説明します。

#### グラフのサイズ設定の仕方
デフォルトでは、グラフの横幅はウィンドウ横幅のサイズに合わせます。ウィンドウのサイズを変えると、グラフ縦幅とグラフ横幅の比率を保ったままグラフのサイズが変わります。

後述の「 **JavaScriptで記述すること⑤ グラフの描画実行** 」で、生成するChartクラスを変数に入れておくと、そのグラフの縦・横のサイズを指定できるようになります。

グラフのオプションで、maintainAspectRatioをfalseに設定すると、縦幅も設定できます。

https://www.chartjs.org/docs/latest/configuration/responsive.html

サイズのカスタマイズ設定をするとき、他の要素の位置がずれてしまうのを防ぐため、グラフのcanvas要素をdiv要素で囲っておくとよいです。

```html
  <div class="chart-parent">
    <canvas id="chart-monthly-overview"></canvas>
  </div>
```

```js
const chartMonthlyOverviewDataConfig = {
...(略)...
  options: {
    maintainAspectRatio: false,
  },
}

var chartMonthlyOverview = new Chart("chart-monthly-overview", chartMonthlyOverviewDataConfig);

chartMonthlyOverview.canvas.parentNode.style.width = "900px";
chartMonthlyOverview.canvas.parentNode.style.height = "400px";
```

#### ドーナツグラフの真ん中
標準の設定では、ドーナツグラフの穴の部分に文字を入れるオプションはありません。
この件についてはChart.jsのGithubのissueになっていて、プラグインを使うことで解決できているようです。

https://github.com/chartjs/Chart.js/issues/78#issuecomment-367406893

このissueにはコードがいくつか載っていますが、3~4年前の古いコードのため、今のバージョンの書き方に合わない部分がありました。公式ドキュメントのプラグインのページも参考にして実装できました。

https://www.chartjs.org/docs/latest/developers/plugins.html#plugins


## JavaScriptで記述すること⑤ グラフの描画実行
このように記述します。

`new Chart(context, config)`

contextはHTMLのcanvas要素を示します。記述の仕方はチュートリアルページに4種類ありますので、その中のどれかで書きます。

https://www.chartjs.org/docs/latest/getting-started/usage.html

configの部分は、グラフの種類・ラベル・数値データ・グラフ書式設定の４つを含んだオブジェクトです。

シンプルな棒グラフなら、configはこのようになります。

```js
{
  type: 'bar',
  data: {
    labels: ["x-axis-item1", "x-axis-item2", "x-axis-item3" ],
    datasets: [{
      label: 'barItem1',
      data: [1, 2, 3]
    }]
  }
}
```

このような書き方が基本で、optionsやグラフ項目の色などを追記します。

datasets配列の要素を複数にすると、
同じ種類のデータが重なったり、ずれた位置に並んだりして描画されます。


棒グラフと折れ線グラフの混合データなら、configは

```js
{
  labels: ["x-axis-item1", "x-axis-item2", "x-axis-item3" ],
  data: {
    datasets: [
      {
        type: 'bar',
        label: 'BarChart',
        data: [1, 2, 3]
      }, {
        type: 'line',
        label: 'LineChart',
        data: [6, 5, 4],
      }
    ],
  },
});
```

このような書き方です。

---

実装の仕方の説明は以上です。
最後にコードをお見せして、この記事を終わります。

## ソースコード

今回の記事のコード全体はこちらです↓ 
※ 折りたたんであります。JavaScriptとCSS埋め込みのhtmlです。

<details><summary>サンプルコード</summary><div>

```html
<!DOCTYPE html>
<head>
  <meta charset="UTF-8">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.6.2/chart.min.js"></script>
  <!-- ↑ここで、https://cdnjs.com/libraries/Chart.js でコピーしたCDNを貼ってください　-->
</head>
<body>
  <h1>My savings</h1>
  <h2>this month</h2>
  <div class="chart-parent">
    <canvas id="chart-monthly-overview"></canvas>
  </div>

  <p>
  <label for="overview-comment">comment:</label>
  <br>
  <textarea id="overview-comment" name="overview-comment" rows="5" cols="100">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
    quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
    Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
    Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
  </textarea>
  </p>

  <canvas id="chart-month"></canvas>

  <div class="chart-parent normal-size">
    <canvas id="chart-ratio"></canvas>
  </div>

  <hr>
  <h2>future plan</h2>
  <canvas id="chart-savings"></canvas>

  <textarea id="chart-monthly-overview-base64" name="chart-monthly-overview-base64" style="visibility: hidden; display: none"></textarea>
</body>
<style>
.normal-size {
  width: 600px;
  height: 500px;
  margin-top: 50px;
  margin-bottom: 50px;
  padding: 20px;
  border: 2px solid gray
}
</style>
<script type="text/javascript">

// データ取得 ※ 今回は以下の通りのデータを取得した想定とする。
// inputData: アプリへの入力などの結果、取得するデータ。今月分の消費の履歴。

const inputData = [
  { label: "meal", data: 5000, memo: "スーパー" },
  { label: "meal", data: 5000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 1000, memo: "飲み物" },
  { label: "meal", data: 1000, memo: "飲み物" },
  { label: "meal", data: 2000, memo: "スーパー" },
  { label: "meal", data: 2000, memo: "スーパー" },
  { label: "meal", data: 1200, memo: "外食" },
  { label: "meal", data: 1200, memo: "外食" },
  { label: "meal", data: 1200, memo: "お菓子" },
  { label: "meal", data: 1200, memo: "外食" },
  { label: "meal", data: 1200, memo: "外食" },
  { label: "meal", data: 1500, memo: "外食" },
  { label: "meal", data: 4500, memo: "スーパー" },
  { label: "meal", data: 3500, memo: "スーパー" },
  { label: "meal", data: 1500, memo: "外食" },
  { label: "meal", data: 1500, memo: "スーパー" },
  { label: "meal", data: 1500, memo: "外食" },
  { label: "meal", data: 1500, memo: "外食" },
  { label: "entertainment", data: 5000, memo: "ゲームソフト" },
  { label: "entertainment", data: 1000, memo: "漫画" },
  { label: "entertainment", data: 2000, memo: "漫画" },
  { label: "entertainment", data: 1000, memo: "小説" },
  { label: "entertainment", data: 1500, memo: "漫画" },
  { label: "entertainment", data: 1500, memo: "漫画" },
  { label: "entertainment", data: 1000, memo: "漫画" },
  { label: "entertainment", data: 800, memo: "雑誌" },
  { label: "entertainment", data: 1200, memo: "漫画" },
  { label: "health", data: 4000, memo: "内科" },
  { label: "health", data: 2000, memo: "薬" },
  { label: "health", data: 2500, memo: "薬" },
  { label: "health", data: 2500, memo: "美容液" },
  { label: "health", data: 3000, memo: "歯医者" },
  { label: "health", data: 1000, memo: "目薬" },
  { label: "health", data: 5000, memo: "保険" },
  { label: "cloth", data: 1500, memo: "靴下" },
  { label: "cloth", data: 3500, memo: "下着" },
  { label: "communication", data: 6000, memo: "会食" },
  { label: "communication", data: 2000, memo: "ボート利用料金" },
  { label: "communication", data: 4000, memo: "プレゼント" },
  { label: "household", data: 1000, memo: "歯磨き粉" },
  { label: "household", data: 1000, memo: "ウェットシート" },
  { label: "household", data: 3000, memo: "掃除用具" },
  { label: "housing", data: 90000, memo: "家賃" },
  { label: "housing", data: 10000, memo: "電気・ガス・水道" },
  { label: "education", data: 5000, memo: "セミナー" },
  { label: "education", data: 1000, memo: "ノート" },
  { label: "education", data: 3000, memo: "技術書" },
  { label: "education", data: 2000, memo: "技術書" },
  { label: "education", data: 9000, memo: "資格試験" },
  { label: "transportation", data: 5000, memo: "自宅wi-fi" },
  { label: "transportation", data: 6000, memo: "携帯" },
  { label: "transportation", data: 1000, memo: "交通費" },
  { label: "transportation", data: 1000, memo: "交通費" },
  { label: "misc", data: 880, memo: "カバン修繕費" },
];

// inputDataLastMonth: 先月分の消費の履歴。
const inputDataLastMonth = [
  { label: "meal", data: 5000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 1000, memo: "飲み物" },
  { label: "meal", data: 1000, memo: "飲み物" },
  { label: "meal", data: 2000, memo: "飲み物" },
  { label: "meal", data: 2000, memo: "飲み物" },
  { label: "meal", data: 1200, memo: "お菓子" },
  { label: "meal", data: 1200, memo: "外食" },
  { label: "meal", data: 1600, memo: "スーパー" },
  { label: "meal", data: 1500, memo: "スーパー" },
  { label: "meal", data: 1500, memo: "スーパー" },
  { label: "entertainment", data: 5000, memo: "ゲームソフト" },
  { label: "entertainment", data: 1000, memo: "漫画" },
  { label: "entertainment", data: 4000, memo: "ゲームソフト" },
  { label: "entertainment", data: 3000, memo: "ゲームソフト" },
  { label: "entertainment", data: 1500, memo: "漫画" },
  { label: "entertainment", data: 1500, memo: "漫画" },
  { label: "entertainment", data: 1000, memo: "ゲーム課金" },
  { label: "entertainment", data: 1500, memo: "雑誌" },
  { label: "entertainment", data: 1500, memo: "雑誌" },
  { label: "entertainment", data: 3000, memo: "画材" },
  { label: "entertainment", data: 2000, memo: "DVD" },
  { label: "entertainment", data: 2000, memo: "DVD" },
  { label: "entertainment", data: 2000, memo: "DVD" },
  { label: "entertainment", data: 2000, memo: "DVD" },
  { label: "entertainment", data: 800, memo: "漫画" },
  { label: "entertainment", data: 1200, memo: "裁縫グッズ" },
  { label: "health", data: 4000, memo: "眼科" },
  { label: "health", data: 1000, memo: "目薬" },
  { label: "health", data: 5000, memo: "保険" },
  { label: "cloth", data: 1500, memo: "タオル" },
  { label: "cloth", data: 8500, memo: "布団" },
  { label: "household", data: 1000, memo: "洗剤" },
  { label: "household", data: 1000, memo: "スポンジ" },
  { label: "household", data: 1500, memo: "フライ返し" },
  { label: "household", data: 1500, memo: "収納グッズ" },
  { label: "housing", data: 90000, memo: "家賃" },
  { label: "housing", data: 10000, memo: "電気・ガス・水道" },
  { label: "transportation", data: 5000, memo: "自宅wi-fi" },
  { label: "transportation", data: 5000, memo: "携帯" },
  { label: "misc", data: 10000, memo: "ハンディマッサージャー" },
];

// inputDataOneYearAgo: 一年前の同じ月の分の消費の履歴。
const inputDataOneYearAgo = [
  { label: "meal", data: 4000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 3000, memo: "スーパー" },
  { label: "meal", data: 1000, memo: "スーパー" },
  { label: "meal", data: 1000, memo: "スーパー" },
  { label: "meal", data: 2000, memo: "スーパー" },
  { label: "meal", data: 2000, memo: "スーパー" },
  { label: "meal", data: 1000, memo: "スーパー" },
  { label: "meal", data: 2000, memo: "スーパー" },
  { label: "meal", data: 1800, memo: "スーパー" },
  { label: "meal", data: 1000, memo: "お菓子" },
  { label: "meal", data: 3500, memo: "お菓子" },
  { label: "meal", data: 1500, memo: "お菓子" },
  { label: "meal", data: 1200, memo: "飲み物" },
  { label: "meal", data: 1200, memo: "外食" },
  { label: "meal", data: 1700, memo: "外食" },
  { label: "meal", data: 1500, memo: "外食" },
  { label: "meal", data: 1500, memo: "外食" },
  { label: "meal", data: 1500, memo: "飲み物" },
  { label: "meal", data: 1500, memo: "飲み物" },
  { label: "entertainment", data: 1500, memo: "DVD" },
  { label: "entertainment", data: 1500, memo: "ボードゲーム" },
  { label: "entertainment", data: 800, memo: "イラスト" },
  { label: "entertainment", data: 1200, memo: "漫画" },
  { label: "health", data: 4000, memo: "エステ" },
  { label: "health", data: 1000, memo: "マスク" },
  { label: "health", data: 2500, memo: "美容液" },
  { label: "health", data: 2500, memo: "サプリ" },
  { label: "health", data: 5000, memo: "保険" },
  { label: "cloth", data: 1500, memo: "服" },
  { label: "cloth", data: 3500, memo: "服" },
  { label: "cloth", data: 2000, memo: "服" },
  { label: "cloth", data: 2500, memo: "服" },
  { label: "cloth", data: 4000, memo: "アクセサリー" },
  { label: "communication", data: 6000, memo: "会食" },
  { label: "communication", data: 2000, memo: "イベント参加費" },
  { label: "communication", data: 4000, memo: "乗り物代" },
  { label: "communication", data: 3000, memo: "イベントのグッズ" },
  { label: "communication", data: 7000, memo: "イベント参加費" },
  { label: "household", data: 500, memo: "歯ブラシ" },
  { label: "household", data: 1500, memo: "財布" },
  { label: "housing", data: 80000, memo: "家賃" },
  { label: "housing", data: 10000, memo: "電気・ガス・水道" },
  { label: "education", data: 1000, memo: "自己啓発本" },
  { label: "transportation", data: 5000, memo: "自宅wi-fi" },
  { label: "transportation", data: 10000, memo: "携帯" },
  { label: "transportation", data: 2000, memo: "交通費" },
  { label: "transportation", data: 1000, memo: "宅配料金" },
];

// ========================================================
// グラフの設定
// label: グラフのラベル。 backgroundColor: 棒グラフや円グラフの背景色。 variableCost: 変動費であるかどうか。radarチャートの表示項目。

const chartItemSetting = [
  { label: "meal", backgroundColor: "rgba(255, 0, 100, 0.8)", variableCost: true },
  { label: "entertainment", backgroundColor: "rgba(255, 255, 0, 0.8)", variableCost: true,},
  { label: "health", backgroundColor: "rgba(0, 255, 255, 0.8)", variableCost: true },
  { label: "cloth", backgroundColor: "rgba(0, 100, 100, 0.8)", variableCost: true },
  { label: "communication", backgroundColor: "rgba(255, 100, 100, 0.8)", variableCost: true,},
  { label: "household", backgroundColor: "rgba(0, 100, 200, 0.8)" },
  { label: "housing", backgroundColor: "rgba(100, 100, 255, 0.8)" },
  { label: "education", backgroundColor: "rgba(100, 0, 100, 0.8)" },
  { label: "transportation", backgroundColor: "rgba(100, 255, 100, 0.8)" },
  { label: "misc", backgroundColor: "rgba(100, 100, 100, 0.8)" },
];

const chartRatioItemSetting = [
  {
    label: "this month",
    borderColor: "rgba(100, 100, 100, 0.8)",
  },
  {
    label: "last month",
    borderColor: "rgba(250, 200, 150, 0.7)",
    backgroundColor: "rgba(250, 250, 150, 0.2)",
  },
  {
    label: "1 year ago",
    borderColor: "rgba(200, 100, 200, 0.6)",
    backgroundColor: "rgba(200, 100, 200, 0.2)",
  },
];

const chartSavingsSetting = {
  yearlySpending: {
    label: "年間支出額",
    backgroundColor: "rgba(255, 50, 0, 0.5)",
  },
  totalSavingsAmount: {
    label: "累計貯蓄額",
    borderColor: "rgb(75, 200, 200)",
    backgroundColor: "rgba(75, 200, 200, 0.3)",
  },
  annualGrossIncome: {
    label: "年収(額面)",
    borderColor: "rgb(200, 200, 50)",
    backgroundColor: "rgba(200, 200, 50, 0.3)",
  },
};

// -----------------------------------------------------
// 関数定義
// ソート用関数
function compareFn(a, b) {
    return b.data - a.data
}

// ベースとなる連想配列と同じラベルを持つインプットデータのdataを取得する
function getDataFn(eBaseData, inputData) {
  let data = inputData
    .filter((e) => e.label == eBaseData.label)
    .map((e) => e.data)
    .reduce((sum, value) => sum + value, 0); // 支出がなかった消費項目のために、初期値0を設定する
  return { label: eBaseData.label, data: data, backgroundColor: eBaseData.backgroundColor };
  //backgroundColorも含めておき、dataの値でソートしてもラベルと背景色の対応を保つようにする
}

// radarチャートの値を計算する
function rateFn(baseData, eDividend) {
  let divisor = baseData.filter(e => e.label == eDividend.label).pop().data
  return eDividend.data / divisor * 100
}

// 貯蓄プラングラフの月給の計算
function getSalaryFn(age) {
  // 月の手取り額がおよそ2~3年に1万円増える計算
  var salary = age * 3750 + 260000;

  // 35歳の時、育児休暇を取り収入が減る。
  if (age == 35) {
    salary = salary - 30;
  }

  // 65歳を過ぎたら労働しない。
  if (age > 65) {
    salary = 0;
  }

  return salary;
}


function getTitleOptionFn(text) {
  return {
    display: true,
    font: {
      size: 24,
    },
    text: text,
  }
}

// -----------------------------------------------------
// 取得したデータの加工

var inputDataSum = chartItemSetting.map(function(e){
  return getDataFn(e, inputData);
})

let thismonthSpending = inputDataSum
  .map((e) => e.data)
  .reduce((sum, value) => sum + value);

// - - - - - - - - - - - - - - - - - - - - - - - - -

// 今月分の消費の穴あき円グラフのデータは、支払い金額の多い順に時計回りで並べる
// sort()は元の配列を置き換えてしまうので、shallow copyをsort()する
const chartMonthlyOverviewData = [...inputDataSum].sort(compareFn);

// - - - - - - - - - - - - - - - - - - - - - - - - -
// radarチャートのデータ

const variableCostList = chartItemSetting.filter((e) => e.variableCost == true);

var percentThismonth = variableCostList.map(function () {
  return 100;
});

var percentLastmonth = variableCostList
  .map(function (e) { return getDataFn(e, inputDataLastMonth)})
  .map(function (e) { return rateFn(inputDataSum, e) });
  
var percentOneYearAgo = variableCostList
  .map(function (e) { return getDataFn(e, inputDataOneYearAgo)})
  .map(function (e) { return rateFn(inputDataSum, e)});

// - - - - - - - - - - - - - - - - - - - - - - - - -
// 貯蓄データ

const totalSavingData = [];
const currentAge = 32;
const currentSavingsAmount = 1500000;

let totalSavingsAmount = currentSavingsAmount;

// 貯蓄プランの未来の収入や出費を、想定で作る
for (let age = currentAge; age <= 80; age++) {
  let eSavingData = {};
  eSavingData.age = age;

  // 1年間の手取り額
  // 給料の22%が厚生年金や税金で引かれる
  var annualGrossIncome = getSalaryFn(age) * 12;
  var annualIncome = getSalaryFn(age) * 0.78 * 12;

  // 61 ~ 65歳の時、収入が32歳ごろと同じになる。
  if (age >= 61 && age <= 65) {
    annualGrossIncome = getSalaryFn(32) * 12;
    annualIncome = getSalaryFn(32) * 0.78 * 12;
  }

  eSavingData.annualGrossIncome = annualGrossIncome;
  eSavingData.annualIncome = annualIncome;

  // 出費
  // 標準で月24万円(今月の出費額)出費する。
  var monthlySpending = thismonthSpending;

  // 33 ~ 34歳の時、引っ越しや家具新調で出費する。
  if (age >= 33 && age <= 34) {
    monthlySpending = monthlySpending + 30000;
  }

  // 36 ~ 53歳の時、養育費がかかる。
  if (age >= 36 && age <= 53) {
    monthlySpending = monthlySpending + 60000;
  }

  // 54 ~ 57歳の時、子供の学費を払う。
  if (age >= 54 && age <= 57) {
    monthlySpending = monthlySpending + 90000;
  }

  // 59歳で、家賃の安い家に引っ越す。
  if (age >= 59) {
    monthlySpending = monthlySpending - 50000;
  }

  // 62歳から、少しずつ医療費が増えてくる。
  if (age >= 62) {
    monthlySpending = monthlySpending + parseFloat(age * 0.03);
  }

  // 65歳を超えたら慎ましく生活する
   if (age > 65) {
     monthlySpending = monthlySpending - 60000;
   }

  eSavingData.monthlySpending = monthlySpending;

  // その年に貯蓄する額(年齢によってはマイナスの額)
  var yearlySavingAmount = annualIncome - (monthlySpending * 12);

  // 現在は9月末で、今から貯蓄計画を始める
  if (age == currentAge) {
    yearlySavingAmount = (yearlySavingAmount / 12) * 3;
  }

  totalSavingsAmount = totalSavingsAmount + yearlySavingAmount;

  eSavingData.yearlySavingAmount = yearlySavingAmount;
  eSavingData.totalSavingsAmount = totalSavingsAmount;

  totalSavingData.push(eSavingData);
}

// ========================================================
// Chartの引数設定
function onCompleteFn(chartId, base64) {
  console.log(_this.toBase64Image());
      document.getElementById(chartId + "-base64").value = base64;
}

// 今月分の消費の穴あき円グラフ描画設定
const chartMonthlyOverviewId = "chart-monthly-overview";
const doughnutChartPlugin = {
  beforeDraw: function (chart) {
    var width = chart.width
    var height = chart.height
    var ctx = chart.ctx
    ctx.restore();
    ctx.font = (height / 12) + "px sans-serif";
    ctx.textBaseline = "top";
    var text = "¥" + thismonthSpending
    var textX = (width / 2) - ctx.measureText(text).width
    var textY = height / 2;
    ctx.fillText(text, textX, textY);
    ctx.save();
  },
};

const chartMonthlyOverviewDataConfig = {
  type: "doughnut",
  data: {
    labels: chartMonthlyOverviewData.map((e) => e["label"]),
    datasets: [
      {
        data: chartMonthlyOverviewData.map((e) => e["data"]),
        backgroundColor: chartMonthlyOverviewData.map(
          (e) => e["backgroundColor"]
        ),
      },
    ],
  },
  options: {
    plugins: {
      title: getTitleOptionFn("category of expenditure in descending order"),
      legend: {
        position: "right",
      },
    },
    maintainAspectRatio: false,
  },
  plugins: [doughnutChartPlugin],
};

// - - - - - - - - - - - - - - - - - - - - - - - - -
// 今月分の消費の横棒グラフ描画設定
const chartMonthId = "chart-month"


const chartMonthConfig = {
  type: "bar",
  data: {
    labels: inputDataSum.map((e) => e["label"]),
    datasets: [
      {
        label: "#1",
        data: inputDataSum.map((e) => e["data"]),
        backgroundColor: inputDataSum.map((e) => e["backgroundColor"]),
      },
    ],
  },
  options: {
    indexAxis: "y",
    plugins: {
      title: getTitleOptionFn("category of expenditure"),
      legend: {
        display: false,
      },
    },
  },
};


// - - - - - - - - - - - - - - - - - - - - - - - - -
// 今月・先月・1年前で比較するradarチャート描画設定
const chartRatioId = "chart-ratio";

const chartRatioDatasets = [
  {
    label: chartRatioItemSetting[0].label,
    data: percentThismonth,
    borderColor: chartRatioItemSetting[0].borderColor,
    fill: false,
    order: 3,
  },
  {
    label: chartRatioItemSetting[1].label,
    data: percentLastmonth,
    borderColor: chartRatioItemSetting[1].borderColor,
    backgroundColor: chartRatioItemSetting[1].backgroundColor,
  },
  {
    label: chartRatioItemSetting[2].label,
    data: percentOneYearAgo,
    borderColor: chartRatioItemSetting[2].borderColor,
    backgroundColor: chartRatioItemSetting[2].backgroundColor,
  },
];

const chartRatioConfig = {
  type: "radar",
  data: {
    labels: variableCostList.map((e) => e["label"]),
    datasets: chartRatioDatasets,
  },
  options: {
    plugins: {
      title: getTitleOptionFn("compared with this month"),
    },
    scales: {
      r: {
        ticks: {
          callback: function (value) {
            return value + "%"
          }
        },
      }
    },
    maintainAspectRatio: false,
  },
};


// - - - - - - - - - - - - - - - - - - - - - - - - -
// 貯蓄プランの折れ線グラフ描画設定

const chartSavingsId = "chart-savings";

const chartSavingsLabel = totalSavingData.map((e) => e["age"]);
const chartSavingsDatasets = [
  Object.assign(
    { ...chartSavingsSetting.yearlySpending },
    {
      type: "bar",
      data: totalSavingData.map((e) => e["monthlySpending"] * 12),
    }
  ),
  Object.assign(
    { ...chartSavingsSetting.totalSavingsAmount },
    {
      type: "line",
      data: totalSavingData.map((e) => e["totalSavingsAmount"]),
      fill: true,
    }
  ),
  Object.assign(
    { ...chartSavingsSetting.annualGrossIncome },
    {
      type: "line",
      data: totalSavingData.map((e) => e["annualGrossIncome"]),
    }
  )
];

const chartSavingsConfig = {
  data: {
    labels: chartSavingsLabel,
    datasets: chartSavingsDatasets,
  },
  options: {
    plugins: {
      title: getTitleOptionFn("yearly total savings"),
    },
  },
};

// -----------------------------------------------------

// 今月分の消費の穴あき円グラフ描画実行
var chartMonthlyOverview = new Chart(chartMonthlyOverviewId, chartMonthlyOverviewDataConfig);

// グラフのサイズ設定
chartMonthlyOverview.canvas.parentNode.style.width = "900px";
chartMonthlyOverview.canvas.parentNode.style.height = "400px";
// [+] configのoptions.maintainAspectRatioをfalseに設定している
// [+] HTMLでcanvasの親要素をdivで作成している

// - - - - - - - - - - - - - - - - - - - - - - - - -

// 今月分の消費の横棒グラフ描画実行
var chartMonth = new Chart(chartMonthId, chartMonthConfig);

//グラフのサイズ設定: なし。ウィンドウ横幅に合わせる。

// - - - - - - - - - - - - - - - - - - - - - - - - -

// 今月・先月・1年前で比較するradarチャート描画実行
const chartRatio = new Chart(chartRatioId, chartRatioConfig);

// グラフのサイズ設定
// [+] configのoptions.maintainAspectRatioをfalseに設定している
// [+] HTMLでcanvasの親要素をdivで作成している
// [+] 親要素divのstyleで横幅・縦幅を設定している

// -----------------------------------------------------

// 貯蓄プランチャート描画実行
const chartSaving = new Chart(chartSavingsId, chartSavingsConfig);

//グラフのサイズ設定: なし。ウィンドウ横幅に合わせる。
</script>
</html>

```
</div></details>