flask + plotly + javascript csvグラフ化ツール　プログラミング編

別記事で紹介している、**[時系列データグラフ化ツール](https://qiita.com/takenoko300/items/0c9fd56a1c1f5bffb32b)**のプログラミング内容を説明します。

# 動作イメージ
![localhost_8880 および他 6 ページ - 個人 - Microsoft​ Edge 2021-08-11 16-38-49_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/338ed8c1-1d40-bb18-c76e-12a6e3956a8d.gif)

# 動作フロー
![csvグラフ化ツール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1873853/717b13d3-b276-1648-90a6-b6ed373e5eb4.png)


# フォルダ構成
アップロードしたファイルのデータ処理はPythonで行い、webブラウザ上の表示の変更などはjavascriptで行っています。

```
$ tree

├── main.py
├── static
|   ├── css
|   |   └── app.css
|   └── js
|       ├── app.js
|       └── main.js
├── temp
└── templates
    └── index.html
```
# プログラム

## Python
**ポート番号**はpython文中の以下の部分で指定することができます。

```python:main.py
# port=****の部分でポート番号を変更できる。
if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=8880)
```

```python:main.py
from flask import Flask, render_template, request, jsonify
import json
import csv
import io
import os
import plotly

import pandas as pd
import numpy as np
from datetime import date, datetime, timedelta

import plotly.offline as offline
import plotly.graph_objs as go

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False  # 文字化け対策
app.debug = True

SAVE_DIR = "./temp/"

# シリアル値を日付に変換
def serial_date(num):
    if type(num) is str:
        num = float(num)
    if np.isnan(num) == False:
        return(datetime(1899, 12, 30) + timedelta(days=num))

# アップロードしたcsvファイルのタイトルを取得
def titleOfFiles(upload_files):
    title = {}
    i = 0
    for file in upload_files:
        fileName = file.filename
        title[str(i)] = os.path.splitext(os.path.basename(fileName))[0]
        i += 1
    return title

# シリアル値を日付に変換
def csvToDf(csvTemp):
    os.system("nkf --overwrite -w " + csvTemp)
    df = pd.read_csv(csvTemp, encoding="utf-8")

    # 数値データではない行を削除
    temp = df.iloc[0, 0].astype(np.float32)
    if np.isnan(temp):
        df = df.drop(df.index[[0]])
        df.reset_index(drop=True, inplace=True)

    # 欠損値を置換
    df = df.fillna(0)

    # 日付データの処理　シリアル値を経過時間と日付に変更
    dateTime = []
    passTime = []
    pa = pb = sb = 0
    for d in df.iloc[:, 0]:
        dateTime.append(json.dumps(serial_date(d), default=json_serial))
        if sb != 0:
            pa = pb + (d - sb) * 24 * 60 * 60  # シリアル値を経過時間(s)に変換
        passTime.append(pa)
        pb = pa
        sb = d

    dateDf = pd.DataFrame({
        '日付 [-]': dateTime,
        '経過時間 [s]': passTime
    })
    df = df.drop(df.columns[[0]], axis=1)

    return df, dateDf

# date, datetimeの変換関数
def json_serial(obj):
    # 日付型の場合には、文字列に変換します
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()
    # 上記以外はサポート対象外.
    raise TypeError("Type %s not serializable" % type(obj))

# データフレームをリストに変換
def dfToList(df):
    List = df.T.values.tolist()
    return List

# urlにアクセスしたらindex.htmlを開く
@app.route('/')
def index():
    return render_template('index.html')

# アップロードファイルのデータ処理
@app.route('/upload', methods=['POST'])
def upload():
    files = request.files.getlist('fileupload')
    titles = titleOfFiles(files)

    # アップロードファイルを一時フォルダに保存
    filess = []
    i = 0
    for file in files:
        fileName = SAVE_DIR + titles[str(i)] + '.csv'
        file.save(fileName)
        i += 1
        filess.append(fileName)

    # ファイルからデータを抜き出して処理→javascriptにデータを渡す
    indexList = {}
    dataList = {}
    data = []
    i = 0
    for file in filess:
        key = titles[str(i)]
        df, dateDf = csvToDf(file)
        os.remove(file)
        if i == 0:
            for j in df.columns:
                data.append(j)
        indexNest = {}
        for column in dateDf.columns:
            indexNest[column] = dfToList(dateDf[column])
        indexList[key] = indexNest
        dataNest = {}
        for column in df.columns:
            dataNest[column] = dfToList(df[column])
        dataList[key] = dataNest
        i += 1

    return render_template('index.html', data=data, name=titles, dataList=dataList, indexList=indexList)

# グラフの第2軸に表示する項目の処理
@app.route('/select', methods=['POST'])
def select():
    data = request.json
    returnData = {}

    l = data['length'] - 2
    indexList = data[str(l)]
    dataList = data[str(l+1)]
    d1 = {}
    for key in dataList:
        i = 0
        itemList = []
        d2 = {}
        for i in range(l):
            item = data[str(i)]
            itemList.append(item)
            d2[item] = dataList[key][item]
        d1[key] = d2

    returnData['data'] = d1

    returnData['itemList'] = itemList
    dateFlag = 0
    if dateFlag:
        i = {}
        for key in indexList:
            i[key] = indexList[key]['日付 [-]']
        returnData['index'] = i
    else:
        i = {}
        for key in indexList:
            i[key] = indexList[key]['経過時間 [s]']
        returnData['index'] = i

    returnData = json.dumps(returnData, ensure_ascii=False)
    return returnData

# javascriptから各種パラメータを入手してグラフをhtmlで生成→javascriptに渡す
@app.route('/outputHTML', methods=["GET", "POST"])
def outputHTML():

    data = request.json
    keyList = list(data.keys())

    if 'listy2' in keyList:
        listy2 = data['listy2']
    else:
        listy2 = []

    if 'graphData' in keyList:
        graphData = data['graphData']
    else:
        graphData = []

    if 'titles' in keyList:
        titles = data['titles']
    else:
        titles = []

    if 'xtitle' in keyList:
        xtitle = data['xtitle']
    else:
        xtitle = ''

    if 'ytitle' in keyList:
        ytitle = data['ytitle']
    else:
        ytitle = ''

    if 'y2title' in keyList:
        y2title = data['y2title']
    else:
        y2title = ''

    if 'xrangeMax' in keyList:
        xrangeMax = data['xrangeMax']
    else:
        xrangeMax = ''

    if 'xrangeMin' in keyList:
        xrangeMin = data['xrangeMin']
    else:
        xrangeMin = ''

    if 'yrangeMax' in keyList:
        yrangeMax = data['yrangeMax']
    else:
        yrangeMax = ''

    if 'yrangeMin' in keyList:
        yrangeMin = data['yrangeMin']
    else:
        yrangeMin = ''

    if 'y2rangeMax' in keyList:
        y2rangeMax = data['y2rangeMax']
    else:
        y2rangeMax = ''

    if 'y2rangeMin' in keyList:
        y2rangeMin = data['y2rangeMin']
    else:
        y2rangeMin = ''

    htmls = {}
    for name in graphData["index"]:
        xv = graphData["index"][name]
        yValues = graphData["data"][name]
        plotData = []
        i = 0
        for key in yValues:
            if graphData['itemList'][i] in listy2:
                trace = go.Scatter(
                    name=key,
                    x=xv,
                    y=yValues[key],
                    mode='lines',
                    type='scatter',
                    yaxis='y2',
                )
            else:
                trace = go.Scatter(
                    name=key,
                    x=xv,
                    y=yValues[key],
                    mode='lines',
                    type='scatter',
                )
            plotData.append(trace)
            i += 1

        layout = go.Layout(
            hovermode="x",
            title=titles[name],
            xaxis=dict(
                title=xtitle,
                range=[xrangeMin, xrangeMax]
            ),
            yaxis=dict(
                title=ytitle,
                range=[yrangeMin, yrangeMax]
            ),
            yaxis2=dict(
                title=y2title,
                range=[y2rangeMin, y2rangeMax],
                overlaying='y',
                side='right'
            ),
            legend=dict(
                x=1.05
            )
        )
        filename = name + '.html'

        fig = go.Figure(data=plotData, layout=layout)
        offline.plot(fig, filename=filename, auto_open=False, show_link=False, config={
                     "displaylogo": False, "modeBarButtonsToRemove": ["sendDataToCloud"]})

        with open(filename) as f:
            s = f.read()
            f.close()

        htmls[name] = s
        os.remove(filename)

    htmls = json.dumps(htmls, ensure_ascii=False)
    return htmls

# port=****の部分でポート番号を変更できる。
if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=8880)
```

## HTML
**{% if data %}** や **{% endif %}** などはFlaskの記法となっています。
詳細はこちらでは割愛して、参考urlを残しておきます。
[ウェブアプリケーションフレームワーク Flask を使ってみる](https://qiita.com/ynakayama/items/2cc0b1d3cf1a2da612e4)

```html:index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- Plotly.js -->
  <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
  <!-- jQuery -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
  <!-- Foundation -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/foundation-sites@6.6.3/dist/css/foundation.min.css" integrity="sha256-ogmFxjqiTMnZhxCqVmcqTvjfe1Y/ec4WaRj/aQPvn+I=" crossorigin="anonymous">
  <script src="https://cdn.jsdelivr.net/npm/foundation-sites@6.6.3/dist/js/foundation.min.js" integrity="sha256-pRF3zifJRA9jXGv++b06qwtSqX1byFQOLjqa2PTEb2o=" crossorigin="anonymous"></script>
  
  <link rel="stylesheet" href="../static/css/app.css">
</head>

<body>

  <div class="grid-x grid-padding-x">
    <fieldset class="fieldset" id="upload-field">
      <legend>ファイルのアップロード</legend>
      <fieldset id="upload-area" class="fieldset" style="height: 100px; width: 100%; background: #DDD;">ここにファイルをドロップ</fieldset>
      <p>または</p>
      <form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="fileupload" id="fileupload" multiple="multiple">
        <input class="button" type="submit" value="Upload File" id="csvSubmit">
      </form>
    </fieldset>
  </div>

  <fieldset class="fieldset" id="switch-toggle-all">
    <legend>描画するデータ系列</legend>

    <div class="switch-toggle-wrapper">
      <div class="switch tiny">
        <input class="switch-input" id="exampleSwitch1" type="checkbox" name="Toggle All" data-toggle-all>
        <label class="switch-paddle" for="exampleSwitch1">
          <span class="show-for-sr">Toggle All</span>
        </label>
      </div>
      <span>全選択</span>
    </div>

    <hr />

    {% if data %}
    <form class="itemForm" id="itemForm" action="/select" method="post">
      {% for d in data %}
      <div class="switch-toggle-wrapper">
        <div class="switch tiny">
          <input type="checkbox" class="switch-input" id="{{d}}" name="item" value="{{d}}">
          <label class="switch-paddle" for="{{d}}">
            <span class="show-for-sr">{{d}}</span>
          </label>
        </div>
        <span>{{d}}</span>
      </div>
      {% endfor %}

      <div style="width:100%"></div>
      <input class="button" type="submit" value="グラフ作成" id="itemSubmit">
    </form>
    {% endif %}

  </fieldset>

  <div class="grid-x grid-padding-x">
    <div class="medium-4 cell" style="padding-left: 0;">
      <fieldset class="fieldset" id="layout">
        <legend>編集項目</legend>
        <div class="grid-x grid-padding-x">
          <div class="medium-12 cell">
            <label>X軸のタイトル
              <input type="text" name="xtitle" id="xtitle" value='時間 [s]'>
            </label>
          </div>
          <div class="medium-12 cell">
            <label>Y軸のタイトル
              <input type="text" name="ytitle" id="ytitle" value='温度 [℃]'>
            </label>
          </div>
          <div class="medium-12 cell">
            <label>第2Y軸のタイトル
              <input type="text" name="y2title" id="y2title" value='' placeholder="None">
            </label>
          </div>
        </div>

        <div class="grid-x grid-padding-x">
          <div class="medium-12 cell">
            <fieldset class="fieldset" style="margin-top: 0; padding-top: 0; padding-bottom: 0;">
              <legend>X軸の範囲</legend>
              <div class="grid-x grid-padding-x">
                <div class="medium-6 cell">
                  <label>最小値
                    <input type="number" name="xrangeMin" id="xrangeMin" value='' placeholder="Auto">
                  </label>
                </div>
                <div class="medium-6 cell">
                  <label>最大値
                    <input type="number" name="xrangeMax" id="xrangeMax" value='' placeholder="Auto">
                  </label>
                </div>
              </div>
            </fieldset>
          </div>

          <div class="medium-12 cell">
            <fieldset class="fieldset" style="margin-top: 0;  padding-top: 0; padding-bottom: 0;">
              <legend>Y軸の範囲</legend>
              <div class="grid-x grid-padding-x">
                <div class="medium-6 cell">
                  <label>最小値
                    <input type="number" name="yrangeMin" id="yrangeMin" value='' placeholder="Auto">
                  </label>
                </div>
                <div class="medium-6 cell">
                  <label>最大値
                    <input type="number" name="yrangeMax" id="yrangeMax" value='' placeholder="Auto">
                  </label>
                </div>
              </div>
            </fieldset>
          </div>

          <div class="medium-12 cell">
            <fieldset class="fieldset" style="margin-top: 0;  padding-top: 0; padding-bottom: 0;">
              <legend>第2Y軸の範囲</legend>
              <div class="grid-x grid-padding-x">
                <div class="medium-6 cell">
                  <label>最小値
                    <input type="number" name="y2rangeMin" id="y2rangeMin" value='' placeholder="Auto">
                  </label>
                </div>
                <div class="medium-6 cell">
                  <label>最大値
                    <input type="number" name="y2rangeMax" id="y2rangeMax" value='' placeholder="Auto">
                  </label>
                </div>
              </div>
            </fieldset>
          </div>

        </div>

      </fieldset>
    </div>

    <div class="medium-8 cell" style="padding-top: 16px; padding-right: 0;">
      <!-- <fieldset class="fieldset"> -->
      <!-- <legend>グラフの選択</legend> -->
      {% if name %}
      <label>グラフの選択
        <select name="select" id="select">
          {% for k, v in name.items() %}
          <option id="selectList" value="{{k}}">{{v}}</option>
          {% endfor %}
        </select>
        {% endif %}
      </label>
      <!-- </fieldset> -->

      <div id="graphs">
        {% if name %}
        {% for k, v in name.items() %}
        <div id="{{k}}">
          <label>グラフタイトル
            <input type="text" name='{{v}}' value='{{v}}'>
          </label>
          <div id='{{v}}'></div>
        </div>
        {% endfor %}
        {% endif %}
      </div>
    </div>

  </div>

  <fieldset class="fieldset" id='y2item'>
    <legend>第2y軸に追加するデータ系列</legend>
    <div class="itemForm"></div>
  </fieldset>

  {% if dataList %}
  <form id="outputHTML" action="/outputHTML" method="post">
    <input class="button" type="submit" value="HTML出力" id="htmlSubmit">
  </form>
  {% endif %}

  <footer>
    <script>
      {% if dataList %}
      var d = {{ dataList | tojson }};
      dataList = d;
      {% endif %}

      {% if indexList %}
      var ind = {{ indexList | tojson }};
      indexList = ind;
      {% endif %}
    </script>

    <script src="../static/js/main.js"></script>
    <!-- <script src="../static/js/vendor/jquery.js"></script>
    <script src="../static/js/vendor/what-input.js"></script>
    <script src="../static/js/vendor/foundation.js"></script> -->
    <script src="../static/js/app.js"></script>
  </footer>

</body>

</html>
```

## JavaScript
Ajaxを用いて非同期でHTMLからデータを取得して、Pythonへデータ送付を行ったりしています。

``` javascript:main.js
// 画面遷移なしのおまじない
$(window).load(select());
$(window).load(outputHTML());

$(document).ready(function(){
  $("#upload-area").on("dragover", function(e){
      e.preventDefault();
  });
  $("#upload-area").on("drop", function(e){
      e.preventDefault();
      document.getElementById("fileupload").files = e.originalEvent.dataTransfer.files;
  });
});

// グローバル変数
var dataList;
var indexList;
var list;
var listy2;
var graphData;
var titles = {};
var xtitle;
var ytitle;
var y2title;
var xrangeMax;
var xrangeMin;
var yrangeMax;
var yrangeMin;
var y2rangeMax;
var y2rangeMin;
let visibles = {}; //トレース線の表示/非表示用

//グラフのタイトルを取得
function getTitles() {
  var Get_Childnodes = window.document.getElementById('graphs').children; //グラフのIDから子要素のタイトルを取得
  var len = Get_Childnodes.length
  for (var i = 0; i < len; i++) {
    key = Get_Childnodes[i].firstElementChild.firstElementChild.name;
    titles[key] = Get_Childnodes[i].firstElementChild.firstElementChild.value;
  }
};

//グラフの軸タイトルや表示範囲などを取得
function getValues() {
  xtitle = $("#xtitle").val();
  ytitle = $("#ytitle").val();
  y2title = $("#y2title").val();
  xrangeMin = $("#xrangeMin").val();
  xrangeMax = $("#xrangeMax").val();
  yrangeMin = $("#yrangeMin").val();
  yrangeMax = $("#yrangeMax").val();
  y2rangeMin = $("#y2rangeMin").val();
  y2rangeMax = $("#y2rangeMax").val();
};

//トレース線の表示/非表示を取得
function getVisibles(){
  let displayGraph
  let displayGraphs = document.querySelectorAll('#graphs>div'); //#graphsの子要素からdivを抜き出す
  for(let i = 0; i < displayGraphs.length; i++){                //表示中のグラフを選択
    let displayState = displayGraphs[i].style.display;          //グラフのstyleからdisplayの値を取得
    if(displayState == ""){                                     //display""であれば（display"None"でなければ）
      displayGraph = displayGraphs[i];
    }
  }

  let traces = displayGraph.querySelectorAll('.groups>g');     //表示中のグラフからトレース線を抜き出す
  if(0 === Object.keys(visibles).length){    //visiblesが空かどうか調べる　→　https://qiita.com/te2u/items/b373914df693ddedf62e
    for(let i=0; i < graphData['itemList'].length; i++){  //空であればトレース線の表示/非表示を格納
      let titleSub = graphData['itemList'][i];            //graphData['itemList']からトレース線のタイトルを取得
      let opacityNow = parseFloat(traces[i].style.opacity);           //トレース線から透過率を取得
      if(opacityNow == 1){
        opacityNow = "True";
      }else{
        opacityNow = "legendonly";
      }
      visibles[titleSub] = opacityNow;       //トレース線のタイトルをkeyにして透過率を数値に変換して追加
    }
  }else{
    for(let i=0; i < graphData['itemList'].length; i++){  //空でなければvisiblesと現在のトレース線の透過率を比較して、変化があれば格納
      let titleSub = graphData['itemList'][i];            //graphData['itemList']からトレース線のタイトルを取得
      let opacityOld = visibles[titleSub];
      let opacityNow = parseFloat(traces[i].style.opacity);           //トレース線から透過率を取得
      if(opacityNow == 1){
        opacityNow = "True";
      }else{
        opacityNow = "legendonly";
      }
      if(opacityOld != opacityNow){                       //visiblesの値と比較
        visibles[titleSub] = opacityNow;                  //値が変化していればvisiblesの値を書き換える
      }
    }    
  }
}

// 画面遷移なしでpythonにデータを送る。itemFormで選択された項目のみのデータをpythonから取得。
function select() {
  $("#itemSubmit").click(function () {
    title = $("#title").val();
    xtitle = $("#xtitle").val();
    ytitle = $("#ytitle").val();
    var $checked = $("form#itemForm [name=item]:checked");
    list = $checked.map(function (index, el) {
      return $(this).val();
    });
    var json = list;
    json.push(indexList);
    json.push(dataList);
    json = JSON.stringify($(list));
    $.ajax({
      type: "POST",                 //pythonファイルにデータを送信
      url: "/select",               //python"main.py"の/selectを実行
      data: json,
      contentType: "application/json",
      success: function (data) {    //データの送信が成功すれば実行
        var data = JSON.parse(data);  //pythonからのデータをjavascript用に変換
        graphData = data;
        initCheckbox(data['itemList']); //第2y軸選択画面のhtmlを作成
        getTitles();
        makeGraph(graphData);
        visibles = {};                //visiblesを初期化
        getVisibles();                //visiblesにデータを格納
      }
    });
    return false;
  });
}

// 画面遷移なしでpythonにデータを送る。html作成。
function outputHTML() {
  $("#htmlSubmit").click(function () {
    var json = {
      listy2: listy2,
      graphData: graphData,
      titles: titles,
      xtitle: xtitle,
      ytitle: ytitle,
      y2title: y2title,
      xrangeMax: xrangeMax,
      xrangeMin: xrangeMin,
      yrangeMax: yrangeMax,
      yrangeMin: yrangeMin,
      y2rangeMax: y2rangeMax,
      y2rangeMin: y2rangeMin,
      visibles: visibles
    };
    json = JSON.stringify(json);
    $.ajax({
      type: "POST",
      url: "/outputHTML",
      data: json,
      contentType: "application/json",
      success: function (data) {
        var data = JSON.parse(data);
        console.log(data);
        for (key in data) {
          strings = data[key];
          let link = document.createElement('a');
          link.download = key + '.html';
          var blob = new Blob([strings], { type: 'text/html' });
          if (window.navigator.msSaveBlob) { 
            window.navigator.msSaveBlob(blob, link.download); 
          }else{
            link.href = URL.createObjectURL(blob);
            link.click();
            URL.revokeObjectURL(link.href);  
          }
        }
      }
    });
    return false;
  });
}

// 全選択スイッチ
$(function () {
  $("#all").on("click", function () {
    $("input[name='item']").prop("checked", this.checked);
  });

  $("input[name='item']").on("click", function () {
    if ($("#itemForm :checked").length == $("#itemForm :input").length) {
      $("#all").prop("checked", true);
    } else {
      $("#all").prop("checked", false);
    }
  });
});

// プルダウンの選択項目以外は非表示 発火はload時とchange時
$(function () {
  $("#select").on("change input", function () {
    var element = document.getElementById('select')
    id = element.value;
    var childElementCount = element.childElementCount;
    for (var i = 0; i < childElementCount; i++) {
      if (i != id) {
        document.getElementById(String(i)).style.display = "none";
      } else {
        document.getElementById(String(i)).style.display = "";
      }
    }
  }).change();
});

//第2y軸選択画面のhtmlを作成
function initCheckbox(d) {
  var chkboxstr = '<legend>第2y軸に追加するデータ系列</legend><div class="itemForm">';
  var i = 0;
  for (var i in d) {
    chkboxstr += "<div class=\"switch-toggle-wrapper\"><div class=\"switch tiny\"><input class=\"switch-input\" id=\"" + d[String(i)] + "y2" + "\" type=\"checkbox\" name=\"y2item\" value=" + d[String(i)] + "'><label class=\"switch-paddle\" for=\"" + d[String(i)] + "y2" + "\"><span class=\"show-for-sr\">" + d[String(i)] + "</span></label></div><span>" + d[String(i)] + "</span></div>";
  }
  chkboxstr += "</div>";
  document.getElementById('y2item').innerHTML = chkboxstr;
};

// タイトル変更など
$(document).ready(function () {
  //keyup()でキーがキーボードから離されるたびに発動
  $(layout).keyup(function (e) {
    getValues();  //グラフの軸タイトルや表示範囲などを取得
    if (graphData) {
      makeGraph(graphData);
    }
  });

  $(layout).change(function (e) {
    getValues();
    if (graphData) {
      makeGraph(graphData);
    }
  });

  $(graphs).keyup(function (e) {
    getTitles();  //グラフのタイトルを取得
    if (graphData) {
      makeGraph(graphData);
    }
  });

  $(graphs).on('click', '.legend', (function (e) {  //初期はlegendクラスが存在しないため、graphsで指定
    if(timer){                  //タイマー作動中にクリックされれば作動中のタイマーをクリア
      clearTimeout(timer);
    }
    let fn = function() {       //タイマー用に関数を定義
      getVisibles();
      if (graphData) {
        makeGraph(graphData);
      }
    }
    timer = setTimeout(fn,500); //トレース線の表示/非表示を500msec後に取得してグラフを描画
  }
  ));
});

let timer;    //↑のタイマー用

$(document).on('change', 'input[name="y2item"]', function () {
  var $checked = $("fieldset#y2item [name=y2item]:checked");
  listy2 = $checked.map(function (index, el) {
    return $(this).val();
  });
  listy2 = JSON.stringify($(listy2));
  if (graphData) {
    makeGraph(graphData);
  }
});

// plotly描画
function makeGraph(data) {

  for (name in data["index"]) {

    var xv = data["index"][name];
    var yValues = data["data"][name];
    var plotData = [];
    var i = 0;
    for (key in yValues) {
      if (String(listy2).indexOf(data['itemList'][String(i)]) == -1) { //listy2に存在しなければ第1y軸にプロット
        var trace = {
          name: key,
          x: xv,
          y: yValues[key],
          mode: "lines",
          type: "scatter",
          visible: visibles[key]
        };
      } else { //もし存在すれば第2y軸にプロットする
        var trace = {
          name: key,
          x: xv,
          y: yValues[key],
          mode: "lines",
          type: "scatter",
          yaxis: "y2",
          visible: visibles[key]
        };
      }
      plotData.push(trace);
      i += 1;
    }

    var layout = {
      title: titles[name],
      width: 780,
      height: 480,
      xaxis: {
        title: xtitle,
        range: [xrangeMin, xrangeMax]
      },
      yaxis: {
        title: ytitle,
        range: [yrangeMin, yrangeMax]
      },
      yaxis2: {
        title: y2title,
        range: [y2rangeMin, y2rangeMax],
        overlaying: "y",
        side: "right"
      },
      legend: {
        x: 1.05
      }
    };

    Plotly.newPlot(name, plotData, layout);

  }
};
```

以下はグラフ表示項目選択時の全選択スイッチ用のプログラム

```javascript:app.js
$(document).foundation()
$('#switch-toggle-all [data-toggle-all]').click(function () {
    $('#switch-toggle-all input[type="checkbox"]').prop('checked', this.checked)
})
```

## CSS
基本的にはFoundationというCSSフレームワークを用いています。微調整程度ではありますが、一応載せておきます。

```css:app.css
body {
    margin: 20px;
    padding: 2em calc(50% - 650px + 2em);
    right: 50%;
    transform: translateX(50%);
    position: relative;
}

.switch-toggle-wrapper {
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
    margin: 0 1rem 1rem 0;
    padding: 0.5rem 0.5rem 0.5rem 0.5rem;
    border: 1px solid rgba(97, 97, 97, 0.25);
    border-radius: 5px;
    -webkit-justify-content: space-between;
    -ms-flex-pack: justify;
    justify-content: space-between;
    -webkit-align-items: center;
    -ms-flex-align: center;
    align-items: center;
    width: 175px;
    flex-direction: row;
    font-size: 11px;
}

.switch-toggle-wrapper .switch {
    margin-bottom: 0;
}

.grid-x grid-padding-x {
    font-size: 12px;
}

.itemForm {
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
}

#y2item.fieldset {
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
}
```