plotly.js を Vue3 に組み込んで表示する

「グラフ系のライブラリの使用経験が薄いな」と感じたので、今回は plotly.js を vue.js に組み込んで表示を試みてみます。

参考

準備

今回も vite を使って初期設定していきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
npm init @vitejs/app app --template vue-ts
cd app

# vite での vue.jsの初期インストール
npm install

# 各種ライブラリのインストール
npm install plotly.js-dist-min

# 型定義のインストール
npm install @types/plotly.js

npm run dev

実装

シンプルに、グラフを描画

それでは具体的実装です。
以下のコンポーネントで動作できました。

src/components/Graph.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div ref="graph"></div>
</template>

<script lang="ts">
import { defineComponent, onMounted, reactive, ref, watch } from "vue";
import Plotly from "plotly.js-dist-min";

export default defineComponent({
setup() {
const graph = ref<HTMLDivElement>();

const data = reactive([
{
x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
]);

onMounted(() => {
if (!graph.value) return;
Plotly.newPlot(graph.value, data);
});

setInterval(() => {
data[0].y[3] += 1;
for (let i = 0; i < data[0].y.length; i++) {
data[0].y[i] = Math.random();
}
}, 2000);

watch(data, () => {
if (!graph.value) return;
Plotly.redraw(graph.value);
});

return { graph };
},
});
</script>

2 秒ごとに、グラフが適当に書き換わります。
表示できました。

データは親から渡すパターン

コンポーネントはグラフ描画だけで、データは親から渡すパターンも試しました。

src/components/Graph.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div ref="graph"></div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, watch, PropType } from "vue";
import Plotly from "plotly.js-dist-min";

export default defineComponent({
props: {
data: {
type: Array as PropType<Plotly.Data[]>,
default: () => [],
required: true,
},
},
setup(props) {
const graph = ref<HTMLDivElement>();

onMounted(() => {
if (!graph.value) return;
Plotly.newPlot(graph.value, props.data);
});

watch(props.data, () => {
if (!graph.value) return;
Plotly.redraw(graph.value);
});

return { graph };
},
});
</script>

親コンポーネントで、グラフ用のコンポーネントに同じデータを渡してみます。

src/App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<!-- 複数個のグラフをまとめて表示 -->
<Graph :data="data" />
<Graph :data="data" />
</template>

<script lang="ts">
import { defineComponent, reactive } from "vue";
import Graph from "./components/Graph.vue";
import BarGraph from "./components/BarGraph.vue";

export default defineComponent({
name: "App",
components: {
Graph,
BarGraph,
},
setup() {
const data = reactive([
{
x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
]);

setInterval(() => {
data[0].y[3] += 1;
for (let i = 0; i < data[0].y.length; i++) {
data[0].y[i] = Math.random();
}
}, 2000);

return { data };
},
});
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

同じデータ内容で、複数のグラフを表示できました。

別のグラフのパターンも試す

データを親コンポーネントから渡すようにしたので、テータを少し加工して複数種類のグラフを表示できるはず。
試してみます。

src/components/BarGraph.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<div ref="graph"></div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, toRef, watch, PropType } from "vue";

import Plotly from "plotly.js-dist-min";

export default defineComponent({
props: {
data: {
type: Array as PropType<Plotly.Data[]>,
default: () => [],
required: true,
},
},
setup(props) {
const graph = ref<HTMLDivElement>();

let data: Plotly.Data[] = [];

const createData = () => {
// データのコピーを作成
let d = Object.assign({}, toRef(props.data, 0).value);
// 棒グラフの形式を指定
d.type = "bar";
data.pop();
data.push(d);
};

onMounted(() => {
createData();
if (!graph.value) return;
Plotly.newPlot(graph.value, data);
});

//watch(props.data, () => {
watch(props.data, () => {
createData();
if (!graph.value) return;
Plotly.redraw(graph.value);
});

return { graph };
},
});
</script>

データを渡す親コンポーネントで作った棒グラフ用のコンポーネントにも同じデータを渡します。

src/App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<template>
<Bar-Graph :data="data" />
<Graph :data="data" />
</template>

<script lang="ts">
import { defineComponent, reactive } from "vue";
import Graph from "./components/Graph.vue";
import BarGraph from "./components/BarGraph.vue";

export default defineComponent({
name: "App",
components: {
Graph,
BarGraph,
},
setup() {
const data = reactive([
{
x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
]);

setInterval(() => {
data[0].y[3] += 1;
for (let i = 0; i < data[0].y.length; i++) {
data[0].y[i] = Math.random();
}
}, 2000);

return { data };
},
});
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

同じデータで複数種類のグラフを表示できました。

API からデータを取得して描画する

最後に、API からデータを取得して描画してみます。

適当なディレクトリで、データ配信サーバーとして json-server の設定をします。

1
2
npm install --save-dev json-server
npx json-server --watch db.json --routes routes.json

db.json と routes.json は次のようにしました。

db.json
1
2
3
4
5
6
{
"data": {
"x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
"y": [1, 2, 3, 4, 5, 6, 7, 8, 9, 20]
}
}
routes.json
1
2
3
{
"/api/*": "/$1"
}

vite の、proxy 設定を入れます。

app/vite.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 4001,
// /api へのアクセスを localhost:3000 への転送
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});

データ取得の実装はこちら。

src/App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<Bar-Graph :data="data" />
<Graph :data="data" />
<Graph :data="data" />
</template>

<script lang="ts">
import { defineComponent, reactive, onMounted } from "vue";
import Graph from "./components/Graph.vue";
import BarGraph from "./components/BarGraph.vue";
import Plotly from "plotly.js-dist-min";

export default defineComponent({
name: "App",
components: {
Graph,
BarGraph,
},
setup() {
// reactive である必要も無いかもしれないが、
// 子コンポーネントが reactiveの前提の実装になっているのでそのまま
const data: Plotly.Data[] = reactive([]);

// onMounted のタイミングでデータを取得してdetaに登録
onMounted(async () => {
const result = await (await fetch("/api/data")).json();
if (!result) return;
data.push(result);
});

return { data };
},
});
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

/api/data から取得した内容を元に、グラフの描画をできました。


vite で前から気になっていたことですが、npm run dev では型チェックされない。
npm run build 時にチェックされて初めて気が付く。
というのが、効率悪いのと、軽く心が折れるところです。
「動作 OK、ビルドで型周りのエラー」よりも、「型チェック/動作 OK、ビルド OK」って流れで作業したい限りです。
後から気が付くより、早めに気が付きたい、今度調べてみたいとこです。

TypeScript で、の型パズル的な実装は、まさしくパズルなので昔のテレビなら「スッキリ」ってとこでしょうか。
毎回悩まずさっと実装したいですね。

今回調べていてすぐにvue-plotlyというラッパーを見つけていたんですが、2 年ほどメンテされていないようで最初から試しもしませんでした。
公開したものの、メンテされなくなるツールの存在は少々寂しいものです。

ではでは。