ABEJA Tech Blog

中の人の興味のある情報を発信していきます

Atomic Design思考でVue.js×Plotly.jsでのグラフComponentを実装した結果

第0章:はじめに

こんにちは。はじめまして。
ABEJAでフロントエンドとバックエンドをフラフラしているエンジニアの齋藤(@z-me)*1です。

本ブログは ABEJA Advent Calendar 2019 の9日目です。

不本意ながらABEJAで開催するフロントエンドのミートアップやカジュアル面談でよく、

ABEJAってAIの会社ってイメージはあるけどUI/UXガッチリやってるイメージがない。

と言われる事が多いので、当ブログ編集長*2が言っている通り*3、ABEJAではプロダクトを開発&提供しているということをお伝えしたいと思います。

今回はその中でも、あまり外部に広く知られていない、ABEJA Insight for Retailの提供しているDashboardで、どのようにUI/UXに力を入れて開発しているのかや、その開発手法(Atomic Design)やグラフComponentの実装Knowledgeを中心にご紹介します。

つまり、今回のブログのモチベは↓こんな感じです。 ABEJA's UI/UX

構成

本ブログですが、語りたいことが非常に沢山あるので、まず最初にどんな構成にしているのかをご紹介します。

章の構成 タイトル 内容
0章 はじめに 最初のご挨拶と、本Blogの構成などの紹介
←いまココ
1章 技術ツール等紹介 Dashboardの提供する技術スタックの概要
1章
1節
Atomic Designとは Atomic Designの説明と
Vue.jsとの親和性に関する話
1章
2節
Plotly.jsとは グラフ作成OSLに関する話
2章 Vue.js × Plotly.js on
Atomic Design
2章
1節
グラフのAtomic Design グラフをAtomic Designで構築する時に
考えなければならない点
2章
2節
Vue.js × Plotly.js
描画タイミング問題
Vue.jsのRenderingタイミングと
Plotly.jsの描画タイミングの問題
2章
3節
Plotly.jsをProductionで
使う場合の辛いところ
Productionデザインに耐えうる
Plotly.jsの対応とその軌跡
3章 おわりに おわりのご挨拶

それでは、本編をどうぞ!

第1章:技術ツール等紹介

IMG*4

上図のようにABEJA Insight for Retail のDashboardは、Vue.jsによるSPA*5として構築し、Amazon S3上に静的ファイルホスティングしています。
開発の際には、PO・デザイナー・エンジニア・QAメンバーがFigma*6を用いてシステムのDesign*7をしており、 実装後のReviewでは、Storybook*8を使って、それぞれの挙動や表示に対してReviewを行っています。

現在ABEJA Insight for RetailのDashboardでは、Dashboardをよりお客様が使いやすく、お客様にとって最大のバリューが出せるようなUI/UXを提供するため、BootstrapやBuefy、Vuetifyなどの既存のUI Component Libraryをそのまま利用せず、デザイナーと「この場面で最も適切なUI/UXってなんだろうね?」といった感じで検討しながらAtomic DesignによるComponent思考でDashboard UIのRenewalを実施しています。
また、グラフの表示では、Plotly.jsというグラフ描画用のオープンソースライブラリを用いてグラフComponentを実装しています。

Atomic Designとは

Atomic Designとは、Brad Frost氏が提唱した、UIを構成する要素をより念入りかつ階層的な設計をするための方法論です。*9
Atomic Design All Elements

Atoms(原子)とは

Atomic Design -Atoms-

Molecules(分子)とは

Atomic Design -Molefules-

Organisms(生体・生物)とは

Atomic Design -Organisms-

Templatesとは

Atomic Design -Templates-

Pagesとは

Atomic Design -Pages-

Vue.jsとの親和性

Atomic DesignはReactやVue.jsなどのComponent思考のフレームワークととても親和性が良いです。
Vue.jsでは、propプロパティを用いたComponent間のデータの受け渡しや、$emit$refを用いたComponentを横断したFunctionの呼び出し実行することができます。
これにより、上位要素から下位要素へデータの受け渡しや下位要素から上位要素のfunction呼び出し等を実施することができます。

vue data and function flow

向き プロパティ 利用方法
データ

props 親宣言: <ChildComponent :data="data" />
子呼び出し: props ['data']
データ

props.sync
&
$emit
親宣言: <ChildComponent :data.sync="data" />
子上書き: props ['data'],
method: hoge () { this.$emit('update:data', '引数')}
Function

$ref 親宣言: <ChildComponent ref="child" />
親呼び出し: this.$refs.child.hoge()
Function

$emit 親宣言: <ChildComponent @hoge="hogex()" />
子呼び出し: this.$emit('hoge', '引数')

コードサンプル

親.vue
<template>
  <Child
    :msg="msg"
    :data.sync="data"
    @changeMsg="changeMsg"
    ref="child"
    />
</template>

<script>
import Child form './Child.vue'
export default {
  name: 'Parents',
  components: {
    Child
  },
  data: () => ({
    msg: 'Hello World',
    data: 'HOGEHOGE'
  }),
  methods: {
    changeMsg (val) {
      this.msg = val
    },
    callChild () {
      this.$refs.child.childMethod()
    }
  }
}
子.vue
<template>
  <div>
    <p>msg: {{msg}}<p>
    <input
      type="button"
      value="msg変更"
      @click="changeMsg"
    />
    <hr>
    <p>data: {{data}}<p>
    <input
      type="button"
      value="data変更"
      @click="changeData"
    />
  </div>
</template>

<script>
export default {
  name: 'Child',
  props: {
    msg: {
      type: String,
      required: true
    },
    data: {
      type: String,
      default: 'HOGEHOGE'
    }
  },
  methods: {
    childMethod () {
      console.log('child Called!')
    },
    changeMsg () {
      this.$emit('changeMsg', `Hello Parents!`)
    },
    changeData () {
      this.$emit('update:data', `FUGAFUGA!`)
    }
  }
}

このように、親Componentでしっかり宣言してあげることで、Componentをしっかり分けた状態でもデータの行き来やFunctionの呼び出し等がきちんとできます。(Vuex使えよって話は一旦置いておきます)

Componentに分けることで下記のようにきれいなディレクトリ構成を作ることもできます。

Atomic Designで開発するメリット
1. タスクの管理がしやすい

ABEJAではGithubを用いてコードの構成管理を実施しています。
Atomic Designで開発する場合、それぞれのComponent単位でIssuを作成します。

f:id:DTM3110:20191204221205p:plain

この時、
Moleculesの作成に必要なEpicを作成することで、Atomsの作成進捗を見ることができ、
Organismsの作成に必要なEpicを作成することで、Moleculesの....
と、必要なComponentの進捗管理がPagesを作るまでの一連の流れで追うことができるようになります

2. 品質担保が容易

Atoms = 1つの機能を実現する最小単位

なので、Atoms単位でUnitTestを組むことができます。
また、下位層のComponentを修正することで上位層のComponentも同時に修正することができます。
もとを正せばAtomsを用いて他のComponent郡が作成されているので、Atomsを直すと全てに影響を与えるとも言いますが…。

3. だんだん楽しくなってくる開発

Atomsから作っているわけですから、Organismsを作るあたりでは自分が作ったAtomsをチームメンバーが触ったり、自分が改めて使ったりすることになります。
そうすると、こんな↓感じの気分になってきます。
f:id:DTM3110:20191204223639p:plain:w150

自分で作ったピースを組み合わせてパズルを組み上げていく感じなので、コツコツ作業をすすめるタイプの人にとっては、作るごとに達成感を得られるようになるでしょう。

デメリット

チームで開発する場合、きちんとComponentの情報共有を取っていないと、下記の様な問題に直面することがあります。

問題 sample
Component変更共有問題 f:id:DTM3110:20191204224503p:plain:w500
Component未作成問題 f:id:DTM3110:20191204224716p:plain:w500
Component作成順序問題 f:id:DTM3110:20191204224755p:plain:w500
Atoms超多いよね問題 f:id:DTM3110:20191204225129p:plain:w300
  • Component変更共有問題
    • Componentを作成する際、設計変更は容易に起こる
      (AtomsなのかMoleculesなのか等は人によって考え方が違う場合が多い)
    • 変更したことを共有し忘れると他の人にも被害が伝播する
  • Component未作成問題
    • すべてのComponentを洗い出すということは非常に難しい
    • 漏れ出たComponentをどのように設計して柔軟に開発レーンに載せられるかが重要
  • Component作成順序問題
    • 下位Componentができないと上位Componentは作成できない
    • 作成のプランニング大事
  • Atoms超多いよね問題
    • Atomic Designを行う上で絶対に逃げられない点
      おとなしく諦めて黙々と作ろう
    • 最初からすべてのAtomsを作ろうと考えるといつまで経ってもMoleculesの作成が始まらないので、ページ単位で必要なComponentを洗い出して作っていくと良い

共有・設計・プランニング大事!! (小並感)

まとめ

f:id:DTM3110:20191204230117p:plain

Plotly.jsとは

Plotly JavaScript Open Source Graphing Library

Built on top of d3.js and stack.gl plotly.js is a high-level, declarative charting library plotly.js ships with 40 chart types, including 3D charts, statistical graphs, and SVG maps. (原文ママ)*10

簡単に訳すと

d3.jsとStack.lgを主体に構築された、グラフをグリグリ動かせる、すごいオープンソースライブラリ

です。
下記のGif画像はPlotlyのPython版のLibraryで実際に作成されたグラフです。

f:id:DTM3110:20191204230957g:plain *11

見ての通り、ぐりぐり操作することができます。

メリット

1. レスポンシブにグラフを動かすことができる
f:id:DTM3110:20191204232015g:plain:w300 f:id:DTM3110:20191204232322g:plain:w300
AUTHOREA -Templates Plotly Graphより Plotly | Dashより
2. リファレンスページがたくさんある

f:id:DTM3110:20191204232551p:plain

3. 設定可能な項目がメチャメチャ沢山ある

f:id:DTM3110:20191204232653p:plain リファレンスページにたくさんの情報があり、該当のページをcurl コマンドで手元に落としてくると
f:id:DTM3110:20191204232810p:plain
と、驚きの2.9MB(ほぼテキスト)もの情報が詰め込まれていることがわかります。

デメリット

使ってみて感じた一番辛かった点は、上記のリファレンスページでのプロパティの検索です。
設定項目が膨大にある = 情報の検索性が重要
ということは想像に難くないと思います。例に習ってこのリファレンスページにも検索フォームがあります。

使い勝手はあまりよくありません。

使い勝手が良くない理由

  1. 検索フォームがHeaderにない
    • 検索して画面下部に行くと次に検索するときに上まで戻らなければならない
    • 情報量が多すぎて、画面一番下から一番上まで全力でスクロールした場合 2分32秒25かかる*12
  2. スクロールバーがデフォルト非表示
    • スクロール中に出てくるやつを運良くマウスで捉えることができるかどうかが運命の分かれ道
    • ドキュメント自体が長いからスクロールバーも小さいので、捉えづらさ倍増
    • 画面上部を見てる場合はHeaderが保護色になってスクロールバーを見つけること自体が困難
  3. 普通にブラウザの検索使うと…
    • ドキュメントが重すぎてブラウザが落ちる*13
    • 検索したいキーワードは先にクリップボードにコピーしてペーストする形じゃないと使い物にならない
  4. 諦めてこのHTMLをCURLコマンドで手元に落としてきても…
    • 肝心のドキュメントがdist*14されているため、改行等とかがない(つまりgrepコマンド等を使えない)
    • Atomとかのエディタで開くと問答無用で落ちる
  5. GoogleSpreadSheet(GSS)やGAS(GoogleAppScript)使ってスクレイピングしようと試みると...
    • GSSの場合データ取得制限、GASの場合時間制限に引っかかってほぼ不可能
    • 「開発するためにリファレンス見てるのに、なんでリファレンス見るために開発してるんだろう」という哲学に陥る

リファレンスページの検索性が良くないことにより、下のようなことが往々にして発生してしまいます。

f:id:DTM3110:20191205205801p:plain:h200 f:id:DTM3110:20191205205828p:plain:h200
1 2
f:id:DTM3110:20191205205903p:plain:h200 f:id:DTM3110:20191205213343p:plain:h200
3 4

まとめ

f:id:DTM3110:20191205210456p:plain

サンプルコードマジ重要

第2章:Vue.js × Plotly.js on Atomic Design

構成説明

本ブログでは、下図の3点に関してそれぞれご紹介します。 f:id:DTM3110:20191205212105p:plain:w400

開発環境

本ブログ作成時の環境は下記のとおりです。

Tool, Package, Module version
f:id:DTM3110:20191205211252p:plain:w100 Node v8.16.0
f:id:DTM3110:20191205211310p:plain:w100 Yarn v1.17.3
f:id:DTM3110:20191205211325p:plain:w100 Vue-cli v2.9.6
f:id:DTM3110:20191205211341p:plain:w100 Plotly.js-dist v1.49.4

1. グラフのAtomicDesign

まずはじめに、

Q: 下のグラフはどのようなComponentに分けられる?

f:id:DTM3110:20191205213737p:plain

という命題があった場合、AtomsのComponentだけでも相当量な数になることが予想できます。

解答例

おそらく、簡単に分けるだけでもこのくらいのAtomsとMoleculesのComponentを作成する必要がある。

f:id:DTM3110:20191205214618p:plain

ただし、Plotly.jsを使う場合、

f:id:DTM3110:20191205215038p:plain
グラフの大部分がSVG形式で生成されるため、軸やLabel、凡例等のComponentを作る必要がなくなります。

ところが、ここで問題になってくるのがAtomsの扱いです。
先述したとおり、

Atomic Design -Atoms-

でありPlotlyの生成するSVGはこの条件を満たしていないため、様々なタイプのグラフを作る際、下記のような問題が出てきます。 f:id:DTM3110:20191205215612p:plain

そこで、
それぞれのグラフごとにデータの処理層と描画LayerComponentを準備することで、問題の解決を図っています。

f:id:DTM3110:20191205220150p:plain
処理層と描画Layerに分けるメリット・デメリット

2. Vue.js × Plotly.js描画タイミング問題

Vue.jsでPlotly.jsを使う場合、描画タイミングによってはグラフが描画されないことがあります。
処理層でデータ自体を変換をする場合などは特に、このタイミングがシビアになってきます。

f:id:DTM3110:20191205221420p:plain
Vue.jsとPlotly.jsの描画タイミング

上図は、Vue.jsを実際に触ったことがある人ならお馴染みのVue.jsのライフサイクルに、処理層での処理タイミングとPlotly.jsの描画タイミングを図示したものです。

僕(俺・私)はソースコードが読みたいんだ!! って人用

ChartViewLayer.vue
import * as Plotly from 'plotly.js-dist'
export default {
  props: {},
  computed: {},
  mounted() {
   this.plotChart().then(() => {
      // NOTE: Component呼び出し後の再描画処理
      return this.rePlotChart()
    })
  },
  methods: {
    async plotChart() {
     return await Plotly.newPlot(this.$refs.Chart, this.data, this.layout, this.plotConfig)
    },
    rePlotChart() {
      return Plotly.redraw(this.$refs.Chart)
    }
  }
}
template.vue
<template>
  <div
    ref="Chart"
    class=“chart-style"
  />
</template>

ページにアクセスした時、まずはじめにVue.jsにより、DOM要素とFunctionのレンダリングが始まります。
この時、処理層がRowDataを取得し表示用のデータに変換します。
一方、描画Layer側ですが、こちらはDom要素の読み込みが終わった段階で、描画するためのDOM要素を参照してレンダリング(newPlot()関数の呼び出し)をはじめます。
データ整形後、描画Layerにて再描画(redraw()関数の呼び出し)をすることで、グラフを表示しています

FAQ

Q1

newPlot()を呼び出しただけではグラフは表示されないの?

A1

ここで問題となるのが、Plotly.jsがわのレンダリングが始まったタイミングではまだ描画するためのデータの整形が完了していないという点です。
newPlot()関数を呼び出した際に、Render先だけは指定されていますが、データ自体はまだ受け取れていないので表示されません。
そのため、redraw()関数にて再描画を行うことで、グラフを明示的に表示させています。


Q2

Mountiedのタイミングで描画せず、Computedのデータ変更からグラフを描画しないの? f:id:DTM3110:20191209114938p:plain

A2

描画中のグラフをデータだけ更新したい場合を除き
処理の大きいnewPlot()関数は極力使わないようにしています。
※画面の重さに直結してしまうため


Q3

描画中のグラフをデータだけ更新したい場合はどうするの?

A3

処理の軽いrestyle()関数を使うことが推奨されていますが、
Production用に記述したレイアウトが崩れる事があるため、
グラフデータのみの変更を検知したときのみ、newPlot()関数を用いて再描画をかけています。

3. Plotly.jsをProductionで使う場合の辛いところ

お客様に提供するソリューションのProductionとは、兎にも角にも映えが重要です。
その中で、グラフというのは最も見た目を意識して作る必要があり、デザイナーさん達もすごい熱量を持ってDesignしてくれています。
が、それを実現するためにはPlotly.jsをこねくり回す必要があります。

今回は筆者がとても印象に残っているPlotly.jsをこねくり回した事例を3つ紹介します。

1. f:id:DTM3110:20191205231512p:plain:h50 多角形レーダーチャート問題

理想 f:id:DTM3110:20191205231743p:plain:w500
現実 f:id:DTM3110:20191205231846p:plain:w300
課題1 デフォルトのレーダーチャートは右側から反時計回りに並ぶ
課題2 Plotly.jsには多角形のレーダーチャートは存在しない
(丸い枠しか無い)

解決方法

手順 結果
設定値を変更
→頂点から時計回りにデータを表示
f:id:DTM3110:20191206221947p:plain:w300
1. データの最大値を取得
2. 別のデータとして表示
f:id:DTM3110:20191205232344p:plain:w300
丸い枠線をすべて非表示に設定 f:id:DTM3110:20191206193802p:plain:w300

解決!!

2. f:id:DTM3110:20191205231606p:plain:h50 ColorBar問題

理想 f:id:DTM3110:20191206200006p:plain:w300
現実 f:id:DTM3110:20191206194924p:plain:w500

横向きのColorBarにはできない!!

当然同じようなことをしたいと思う人はいるわけで、Issueも上がっていますが…

f:id:DTM3110:20191206195522p:plain *15

前述したとおり、Plotly.jsはSVGを生成します。
DOM要素としてのSVGはCSSを当てる事ができます。
今回のように、要素を回転させたい場合は対象の要素に対して transform: rotate(90deg); と設定することで90°回転させることができます。
こうなったら自力でCSS当てて修正するしかない…
と決心したのは良いのですが、いざCSSを当てようとした時、大きな問題に直面しました。

  1. Render直後にSVGの基準点が設定される
  2. グラフのx軸・y軸それぞれの描画後に更にSVGの基準点が更新される
  3. グラフのデータが描画された直後に更にSVGの基準点が更新される
  4. その他グラフ描画に必要な情報のLayerが更新されるごとにSVGの基準点が更新される
  5. グラフの描画が終わってもwindowサイズ等を変更するとPlotlyの仕様でSVGの基準点が更に更新される

と、上記のようにSVGの基準点が全然定まらない状態になります。
ここで、先程のtransform: rotate(90deg);を当てようとしても、下図のようにまともに配置することができなくなります。
そもそも描画用のDOMの外に飛び出すので、どこに行ったのかすらわからなくなる始末...

f:id:DTM3110:20191206205201p:plain:h500

ここで、ジョジョ第一部 ファントムブラッドを思い出した私が取った方法はこちら↓

f:id:DTM3110:20191206211307g:plain
意外!それは回転ッ!

CSSのanimationプロパティを使い、該当のプロパティを回しながらColorBarの中心(SVGの基準点とするべきポイント)を探しました。 こちら遊んでるわけでもなんでもなく、下記のようなメリットがあります。

  1. 基準点が動き回って定まらない → 逆に考えるんだ「動かし続けてもいいさ」と考えるんだ*16
    • animationで動かし続けることによって、基準点が更新され続けて一定になる
    • transform: rotate(90deg);が使えるようになる!!
  2. 回っているので、基準点 = 中心 ということが一発で分る
    • 中心点を新しく基準点として設定してあげるとその場できれいに回る
  3. 回り続けるのでwindowサイズを変更して、どのくらい歪むかがすぐわかる
    • まさかの手動レスポンシブ対応である

一方こちらデメリット

f:id:DTM3110:20191206212726g:plain
遊んでいるようにしか見えない

このように試行錯誤をしCSSを当てることで横向きColorBarを実現!
f:id:DTM3110:20191206213615p:plain

解決!!

3. f:id:DTM3110:20191205231606p:plain:h50カラースケール問題

グラフの表示調整だけではなく、実装の方面でも記憶に新しい問題がこちらです。

理想

f:id:DTM3110:20191206214221p:plain:w300 f:id:DTM3110:20191206214234p:plain:w300 f:id:DTM3110:20191206214247p:plain:w300
0点で色を変えたい 平均値のところで色を変えたい 中央値のところで色を変えたい

つまるところ、実装時には楽に色の指定をしたい。

現実
f:id:DTM3110:20191206214411p:plain

まさかの2次元配列

この形式の2次元配列を作ること自体が面倒
Objectの場合は、Object.entries()で作れるかも...

色の指定は 0〜1の割合
  • 0点と平均値は全体のどの割合のところにいるのかを計算する必要がある
    • 中央値は必ず0.5
受け渡す配列は割合の昇順である必要がある
Array.prototype.sort() 使えばいいじゃん
  • 割合の数値は Number じゃなくて String
    • 比較関数用意する必要がある… ツライ

解決方法

実はコレの解決方法はすでにご紹介しています。

f:id:DTM3110:20191206220429p:plain

処理層でこの2次元配列の生成処理をまとめているため、前述したとおり
UnitTestをしっかりできる複雑な記載方法でも処理を担保できる処理速度重視の記述ができる
ため、実際に利用する際は、理想とした形式でColorScaleを指定することができるようになります。

解決!!

終わりに

f:id:DTM3110:20191206224350p:plain

挨拶

長々とお付き合いいただきありがとうございました。
実は今回ご紹介した以外にも大変だった点、良かった点、工夫した点などまだまだあったりします。
興味のある方は、是非ABEJAのフロントエンドのミートアップ等にお越しください!

We Are Hiring!!

ABEJAでは一緒にチャレンジしていくメンバーを募集しています!!

hrmos.co

www.wantedly.com

ABEJAの中の人と話ししたい!オフィス見学してみたいも随時受け付けておりますので、気軽にポチッとどうぞ↓↓

*1: 齋藤 創(サイトウハジメ):2017年公立はこだて未来大学大学院卒 新卒として株式会社ABEJAにJoin 解析インスタンスに乗り込んでメンテナンスを行ったり社内システムの開発・ダッシュボードの開発等をしている、FrontendなのかBackendなのかわからないエンジニア

*2: Tech Blog編集長(自称) 緒方(@conta_)

*3:割とAIコンサルの会社と思われているらしいので、ちゃんとプロダクト作ってますよ!ということを伝えていきたい (ABEJAの技術スタックを公開します(2019年11月版)冒頭より)

*4:ABEJAの技術スタックを公開します(2019年11月版) - ダッシュボード関連システムより

*5:Single Page Application

*6: Figmaでは、設計やプロトタイプ作成、フィードバックの収集をすべてFigma上で行うことができる(Figmaより)

*7:ここで言うDesignは設計を含む

*8:StorybookはReactやVue、Angularを分離してUIコンポーネントを開発するためのオープンソースツール(Storybookより)

*9: Atmic Design Methodologyより

*10: Plotly JavaScript Open Source Graphing Libraryより

*11:@inoory, [Python] Plotlyでぐりぐり動かせるグラフを作るより

*12:筆者の実測値

*13:筆者は4度ほどブラウザが落ちました。

*14:ここで言うdist化とは、本来改行やスペースが入っているはずのドキュメントにおいてスペースや改行が全て取り除かれファイルサイズを小さくする処理を指す

*15: allow for horizontal colorbar #1244

*16:ジョジョ第1部 ファントムブラッドより