第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を中心にご紹介します。
つまり、今回のブログのモチベは↓こんな感じです。
構成
本ブログですが、語りたいことが非常に沢山あるので、まず最初にどんな構成にしているのかをご紹介します。
章の構成 | タイトル | 内容 |
---|---|---|
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章:技術ツール等紹介
上図のように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
Atoms(原子)とは
Molecules(分子)とは
Organisms(生体・生物)とは
Templatesとは
Pagesとは
Vue.jsとの親和性
Atomic DesignはReactやVue.jsなどのComponent思考のフレームワークととても親和性が良いです。
Vue.jsでは、prop
プロパティを用いたComponent間のデータの受け渡しや、$emit
や$ref
を用いたComponentを横断したFunctionの呼び出し実行することができます。
これにより、上位要素から下位要素へデータの受け渡しや下位要素から上位要素のfunction呼び出し等を実施することができます。
向き | プロパティ | 利用方法 | |
---|---|---|---|
データ | 親 ↓ 子 |
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', '引数') |
コードサンプル
<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() } } }
<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を作成します。
この時、
Moleculesの作成に必要なEpicを作成することで、Atomsの作成進捗を見ることができ、
Organismsの作成に必要なEpicを作成することで、Moleculesの....
と、必要なComponentの進捗管理がPagesを作るまでの一連の流れで追うことができるようになります
2. 品質担保が容易
Atoms =
1つの機能を実現する最小単位
なので、Atoms単位でUnitTestを組むことができます。
また、下位層のComponentを修正することで上位層のComponentも同時に修正することができます。
もとを正せばAtomsを用いて他のComponent郡が作成されているので、Atomsを直すと全てに影響を与えるとも言いますが…。
3. だんだん楽しくなってくる開発
Atomsから作っているわけですから、Organismsを作るあたりでは自分が作ったAtomsをチームメンバーが触ったり、自分が改めて使ったりすることになります。
そうすると、こんな↓感じの気分になってきます。
自分で作ったピースを組み合わせてパズルを組み上げていく感じなので、コツコツ作業をすすめるタイプの人にとっては、作るごとに達成感を得られるようになるでしょう。
デメリット
チームで開発する場合、きちんとComponentの情報共有を取っていないと、下記の様な問題に直面することがあります。
問題 | sample |
---|---|
Component変更共有問題 | |
Component未作成問題 | |
Component作成順序問題 | |
Atoms超多いよね問題 |
- Component変更共有問題
- Componentを作成する際、設計変更は容易に起こる
(AtomsなのかMoleculesなのか等は人によって考え方が違う場合が多い) - 変更したことを共有し忘れると他の人にも被害が伝播する
- Componentを作成する際、設計変更は容易に起こる
- Component未作成問題
- すべてのComponentを洗い出すということは非常に難しい
- 漏れ出たComponentをどのように設計して柔軟に開発レーンに載せられるかが重要
- Component作成順序問題
- 下位Componentができないと上位Componentは作成できない
- 作成のプランニング大事
- Atoms超多いよね問題
- Atomic Designを行う上で絶対に逃げられない点
おとなしく諦めて黙々と作ろう - 最初からすべてのAtomsを作ろうと考えるといつまで経ってもMoleculesの作成が始まらないので、ページ単位で必要なComponentを洗い出して作っていくと良い
- Atomic Designを行う上で絶対に逃げられない点
共有・設計・プランニング大事!! (小並感)
まとめ
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で実際に作成されたグラフです。
見ての通り、ぐりぐり操作することができます。
メリット
1. レスポンシブにグラフを動かすことができる
AUTHOREA -Templates Plotly Graphより | Plotly | Dashより |
2. リファレンスページがたくさんある
3. 設定可能な項目がメチャメチャ沢山ある
リファレンスページにたくさんの情報があり、該当のページをcurl
コマンドで手元に落としてくると
と、驚きの2.9MB(ほぼテキスト)もの情報が詰め込まれていることがわかります。
デメリット
使ってみて感じた一番辛かった点
は、上記のリファレンスページでのプロパティの検索です。
設定項目が膨大にある = 情報の検索性が重要
ということは想像に難くないと思います。例に習ってこのリファレンスページにも検索フォームがあります。
が
使い勝手はあまりよくありません。
使い勝手が良くない理由
- 検索フォームがHeaderにない
- 検索して画面下部に行くと次に検索するときに上まで戻らなければならない
- 情報量が多すぎて、画面一番下から一番上まで全力でスクロールした場合
2分32秒25
かかる*12
- スクロールバーがデフォルト非表示
- スクロール中に出てくるやつを運良くマウスで捉えることができるかどうかが運命の分かれ道
- ドキュメント自体が長いからスクロールバーも小さいので、捉えづらさ倍増
- 画面上部を見てる場合はHeaderが保護色になってスクロールバーを見つけること自体が困難
- 普通にブラウザの検索使うと…
- ドキュメントが重すぎてブラウザが落ちる*13
- 検索したいキーワードは先にクリップボードにコピーしてペーストする形じゃないと使い物にならない
- 諦めてこのHTMLを
CURL
コマンドで手元に落としてきても…- 肝心のドキュメントが
dist
化*14されているため、改行等とかがない(つまりgrep
コマンド等を使えない) - Atomとかのエディタで開くと問答無用で落ちる
- 肝心のドキュメントが
- GoogleSpreadSheet(GSS)やGAS(GoogleAppScript)使ってスクレイピングしようと試みると...
- GSSの場合データ取得制限、GASの場合時間制限に引っかかってほぼ不可能
- 「開発するためにリファレンス見てるのに、なんでリファレンス見るために開発してるんだろう」という哲学に陥る
リファレンスページの検索性が良くないことにより、下のようなことが往々にして発生してしまいます。
1 | 2 |
3 | 4 |
まとめ
サンプルコードマジ重要
第2章:Vue.js × Plotly.js on Atomic Design
構成説明
本ブログでは、下図の3点に関してそれぞれご紹介します。
開発環境
本ブログ作成時の環境は下記のとおりです。
Tool, Package, Module | version | |
---|---|---|
Node | v8.16.0 | |
Yarn | v1.17.3 | |
Vue-cli | v2.9.6 | |
Plotly.js-dist | v1.49.4 |
1. グラフのAtomicDesign
まずはじめに、
Q: 下のグラフはどのようなComponentに分けられる?
という命題があった場合、AtomsのComponentだけでも相当量な数になることが予想できます。
解答例
おそらく、簡単に分けるだけでもこのくらいのAtomsとMoleculesのComponentを作成する必要がある。
ただし、Plotly.jsを使う場合、
グラフの大部分がSVG形式で生成されるため、軸やLabel、凡例等のComponentを作る必要がなくなります。
ところが、ここで問題になってくるのがAtomsの扱いです。
先述したとおり、
でありPlotlyの生成するSVGはこの条件を満たしていないため、様々なタイプのグラフを作る際、下記のような問題が出てきます。
そこで、
それぞれのグラフごとにデータの処理層と描画LayerComponentを準備することで、問題の解決を図っています。
2. Vue.js × Plotly.js描画タイミング問題
Vue.jsでPlotly.jsを使う場合、描画タイミングによってはグラフが描画されないことがあります。
処理層でデータ自体を変換をする場合などは特に、このタイミングがシビアになってきます。
上図は、Vue.jsを実際に触ったことがある人ならお馴染みのVue.jsのライフサイクルに、処理層での処理タイミングとPlotly.jsの描画タイミングを図示したものです。
僕(俺・私)はソースコードが読みたいんだ!! って人用
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> <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のデータ変更からグラフを描画しないの?
A2
描画中のグラフをデータだけ更新したい場合を除き
処理の大きいnewPlot()関数は極力使わないようにしています。
※画面の重さに直結してしまうため
Q3
描画中のグラフをデータだけ更新したい場合はどうするの?
A3
処理の軽いrestyle()関数を使うことが推奨されていますが、
Production用に記述したレイアウトが崩れる事があるため、
グラフデータのみの変更を検知したときのみ、newPlot()関数を用いて再描画をかけています。
3. Plotly.jsをProductionで使う場合の辛いところ
お客様に提供するソリューションのProductionとは、兎にも角にも映えが重要です。
その中で、グラフというのは最も見た目を意識して作る必要があり、デザイナーさん達もすごい熱量を持ってDesignしてくれています。
が、それを実現するためにはPlotly.jsをこねくり回す必要があります。
今回は筆者がとても印象に残っているPlotly.jsをこねくり回した事例を3つ紹介します。
1. 多角形レーダーチャート問題
理想 | |
---|---|
現実 | |
課題1 | デフォルトのレーダーチャートは右側から反時計回りに並ぶ |
課題2 | Plotly.jsには多角形のレーダーチャートは存在しない (丸い枠しか無い) |
解決方法
手順 | 結果 |
---|---|
設定値を変更 →頂点から時計回りにデータを表示 |
|
1. データの最大値を取得 2. 別のデータとして表示 |
|
丸い枠線をすべて非表示に設定 |
解決!!
2. ColorBar問題
理想 | |
---|---|
現実 |
横向きのColorBarにはできない!!
当然同じようなことをしたいと思う人はいるわけで、Issueも上がっていますが…
前述したとおり、Plotly.jsはSVGを生成します。
DOM要素としてのSVGはCSSを当てる事ができます。
今回のように、要素を回転させたい場合は対象の要素に対して transform: rotate(90deg);
と設定することで90°回転させることができます。
こうなったら自力でCSS当てて修正するしかない…
と決心したのは良いのですが、いざCSSを当てようとした時、大きな問題に直面しました。
- Render直後にSVGの基準点が設定される
- グラフのx軸・y軸それぞれの描画後に更にSVGの基準点が更新される
- グラフのデータが描画された直後に更にSVGの基準点が更新される
- その他グラフ描画に必要な情報のLayerが更新されるごとにSVGの基準点が更新される
- グラフの描画が終わってもwindowサイズ等を変更するとPlotlyの仕様でSVGの基準点が更に更新される
と、上記のようにSVGの基準点が全然定まらない状態になります。
ここで、先程のtransform: rotate(90deg);
を当てようとしても、下図のようにまともに配置することができなくなります。
そもそも描画用のDOMの外に飛び出すので、どこに行ったのかすらわからなくなる始末...
ここで、ジョジョ第一部 ファントムブラッドを思い出した私が取った方法はこちら↓
CSSのanimation
プロパティを使い、該当のプロパティを回しながらColorBarの中心(SVGの基準点とするべきポイント)を探しました。
こちら遊んでるわけでもなんでもなく、下記のようなメリットがあります。
- 基準点が動き回って定まらない →
逆に考えるんだ「動かし続けてもいいさ」と考えるんだ
*16- animationで動かし続けることによって、基準点が更新され続けて一定になる
transform: rotate(90deg);
が使えるようになる!!
- 回っているので、
基準点 = 中心
ということが一発で分る- 中心点を新しく基準点として設定してあげるとその場できれいに回る
- 回り続けるのでwindowサイズを変更して、どのくらい歪むかがすぐわかる
- まさかの手動レスポンシブ対応である
一方こちらデメリット
このように試行錯誤をしCSSを当てることで横向きColorBarを実現!
解決!!
3. カラースケール問題
グラフの表示調整だけではなく、実装の方面でも記憶に新しい問題がこちらです。
理想
0点で色を変えたい | 平均値のところで色を変えたい | 中央値のところで色を変えたい |
つまるところ、実装時には楽に色の指定をしたい。
現実
まさかの2次元配列
この形式の2次元配列を作ること自体が面倒
Objectの場合は、Object.entries()
で作れるかも...
色の指定は 0〜1の割合
- 0点と平均値は全体のどの割合のところにいるのかを計算する必要がある
- 中央値は必ず
0.5
- 中央値は必ず
受け渡す配列は割合の昇順である必要がある
Array.prototype.sort() 使えばいいじゃん
- 割合の数値は
Number
じゃなくてString
- 比較関数用意する必要がある… ツライ
解決方法
実はコレの解決方法はすでにご紹介しています。
処理層でこの2次元配列の生成処理をまとめているため、前述したとおり
UnitTestをしっかりできる
→ 複雑な記載方法でも処理を担保できる
→ 処理速度重視の記述ができる
ため、実際に利用する際は、理想とした形式でColorScaleを指定することができるようになります。
解決!!
終わりに
挨拶
長々とお付き合いいただきありがとうございました。
実は今回ご紹介した以外にも大変だった点、良かった点、工夫した点などまだまだあったりします。
興味のある方は、是非ABEJAのフロントエンドのミートアップ等にお越しください!
We Are Hiring!!
ABEJAでは一緒にチャレンジしていくメンバーを募集しています!!
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部 ファントムブラッドより