vue + vuex + plotly.jsであそぶ


# はじめに
最近仕事でvue.jsに触れる機会がありました。
内容は社内で利用するシステムのダッシュボードの一部の機能として、ジョブとそれに所属するメンバーの情報をグラフ表示するものです。
今回この記事では、開発した内容を少し変更し、以下のようなものを作成しました。

![sample.gif](https://qiita-image-store.s3.amazonaws.com/0/3748/91853527-2320-3f91-9556-1aeb48546c2b.gif)

構成は以下のようになっています。

- job一覧表示
- 選択したjobのmember一覧表示
- 選択したmemberのステータスをグラフ表示
    - memberのステータス
        - atk (攻撃力)
        - def (防御力)
        - agi (敏捷性)
        - dex (命中率)
        - eva (回避率)

といった感じです。

jobカラムでjobを選択すると、隣のmemberカラムにそのjobに所属するmember一覧が表示され、更にmemberを選択するとplotカラムにてmemberのステータス情報が折れ線グラフとして表示されます。
また、jobを別のものに選択すると、グラフ描画部分がリセットされるようになっています。

この記事では主にplotly.jsとvueの組み合わせ部分についてと、vuexでステートが更新される度に、グラフを再描画させる部分にフォーカスします。

# 主な構成

- vue
- vuex
- plotly.js
- json-server
- vue-axios

# 実装概要

## サンプルデータ
json-serverを稼働させ、fakerで作成した適当なデータを利用します。

```json:db.json
{
    "jobs": [
        {
            "id": 1,
            "name": "Producer"
        },
        {
            "id": 2,
            "name": "Associate"
        },
...
        }
    ],
    "members": [
        {
            "id": "100001",
            "name": "Myrtice Terry",
            "jobId": 1,
            "statusLabels": [
                "atk",
                "def",
                "agi",
                "dex",
                "eva"
            ],
            "statusValues": [
                7,
                1,
                7,
                4,
                3
            ]
        },
...
```

## vuex
本サンプルでは、jobとmemberを管理する二つのストアを用意しています。

### store/job
job一覧と選択されたjobのidを管理します。
jobの一覧よりどれかを選択することにより、`selectedJobId`にjobのidが格納されます。

```js:store/job.js
import Vue from 'vue'

const job = {
  namespaced: true,

  state: {
    jobs: [],
    selectedJobId: 1
  },

  getters: {
    jobList: state => state.jobs,
    selectedJobId: state => state.selectedJobId
  },

  mutations: {
    setJobList (state, jobs) {
      state.jobs = jobs
    },

    setSelectedJobId (state, jobId) {
      state.selectedJobId = jobId
    }
  },

  actions: {
    load ({ commit }) {
      return Vue.axios.get('http://localhost:3000/jobs').then(res => {
        commit('setJobList', res.data)
      })
    }
  }
}

export default job
```

### store/member
job一覧よりどれか選択すると、membersにmember一覧がセットされます。
メンバー一覧よりどれかメンバーを選択されたとき、selectedMemberIdに選択対象のidがセットされます。

```js:store/member.js
import Vue from 'vue'

const member = {
  namespaced: true,

  state: {
    members: [],
    selectedMemberId: null
  },

  getters: {
    members: state => state.members,
    selectedMemberId: state => state.selectedMemberId
  },

  mutations: {
    setMembers (state, members) {
      state.members = members
    },

    setSelectedMemberId (state, id) {
      state.selectedMemberId = id
    }
  },

  actions: {
    load ({commit, state}, id) {
      console.log(id)
      return Vue.axios.get(`http://localhost:3000/members?jobId=${id}`).then(res => {
        commit('setMembers', res.data)
      })
    }
  }
}

export default member
```

## グラフ描画について
plotly.jsを利用しています。今回の作成したサンプルではvuexのstateが更新されたタイミングでグラフの再描画が行われるようになっています。


### グラフ初期化表示
plotly.jsでは通常、グラフ表示を行いたいDOMに対しidを指定し、DOMが描画された後にjs側でグラフ描画処理がされます。
このような処理をvueと合わせて利用する場合は`$refs`を利用し、コンポーネントが描画されたタイミングで対象要素に対してグラフ描画を行うようにします。

```html:plot.vue(template)
<template>
  <div ref="plot"></div>
</template>
```

```vue:plot.vue(script)
  mounted: {
    const trace = {
      x: ['a', 'b', 'c', 'd', 'e'],
      y: [1, 2, 3, 4, 5],
      type: 'scatter'
    }

    Plotly.newPlot(this.$refs.plot, [trace])
  }
```

### グラフ描画更新

watch部分にて、vuex管理化の`store/member.js`内のステートを監視しています。
ステートが更新される度にwatch内でメソッドが発行され、グラフ描画が行われます。plotlyの描画を更新する際は、`restyle()`を実行します。

```vue:plot.vue
import { mapGetters } from 'vuex'
import Plotly from '@/plotly'

export default {
  name: 'Plot',

  computed: {
    ...mapGetters('member', ['members', 'selectedMemberId']),

    // メンバー一覧より、選択されたメンバー情報を取得
    memberInfo () {
      return this.members.find(el => el.id === this.selectedMemberId)
    }
  },

  watch: {
    // member一覧が更新されたらプロット内容をリセット
    members (newData) {
      this.resetLineChart()
    },

    // memberが選択されたら対象のステータス情報をplotlyにセット
    selectedMemberId (newData) {
      this.setPlotData()
    }
  },

  methods: {
    setPlotData () {
      // 更新データを作成
      const trace = {
        name: this.memberInfo.name,
        y: [this.memberInfo.statusValues]
      }

      this.plotChart(trace)
    },

    resetLineChart () {
      // リセットデータを作成
      const trace = {
        name: '',
        y: [[[], [], [], [], []]]
      }

      this.plotChart(trace)
    },

    // グラフ再描画
    plotChart (trace) {
      Plotly.restyle(this.$refs.plot, trace, 0)
    }
  },
```

ここまでで操作内容と中身の処理を整理すると以下のようになります。

- jobカラムにてjobを選択
  - store/job.js内の`selectedJobId`に選択したjobのidが格納される
  - store/member.js内の`members`に選択したjobに所属するmember一覧が格納される
- memberカラムにてmemberを選択
  - store/member.js内の`selectedMemberId`に選択したmemberのidが格納される
  - コンポーネント側でselectedMemberIdを監視し、対象のメンバーのステータス情報をグラフ描画

# 感想
今までjqueryで頑張ってきてた身からvueへ転向してまだ短いですが、まだまだだなぁと感じる部分が多いです。
特にvuex側の処理がまだ慣れていないため、経験を積んで見通しの良いフローにできればと思います。

蛇足ですが、plotlyでリアクティブな処理を行いたい場合はdashを利用するのも手です。
こちらはplotly公式のもので、pythonを記述するだけでインタラクティブな操作が可能なダッシュボードが作れます。興味がある方は是非触れてみることをお勧めします。

- [Dash by plotly](https://plot.ly/products/dash/)