😎

PlotlyのインタラクティブグラフをZennで表示する

2024/01/10に公開

はじめに

Plotly はマウスでぐりぐり動かせるインタラクティブなグラフを出力することができます。しかしこのグラフは HTML+JavaScript なので、このままでは Zenn の記事内には表示できません。動画化したり外部サイトに誘導してもよいのですが、できれば記事内で完結したいところです。そこで Zenn の コンテンツの埋め込み 機能を使って、グラフを埋め込む方法を試してみます。

候補

2024/01/08 現在、この用途に使えるサービスは下記の4つです。

  1. CodePen
  2. JSFiddle
  3. CodeSandbox
  4. StackBlitz

グラフ生成

グラフは何でも良いんですが、インタラクティブの効果が分かりやすい 3D グラフを 公式のサンプル から拝借します。

import numpy as np
import pandas as pd
import plotly.graph_objects as go

# データセットの取得
z = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/"
    "api_docs/mt_bruno_elevation.csv"
).values
x = np.linspace(0, 1, z.shape[0])
y = np.linspace(0, 1, z.shape[1])

# グラフの作成
fig = go.Figure(data=[go.Surface(x=x, y=y, z=z)])

# HTML 出力
fig.write_html(
    "./temp.html",
    include_plotlyjs="cdn",
)

デフォルトでは plotly.js が埋め込まれる設定になっているので、3MB 超の HTML ファイルが出力されます。上記のように include_plotlyjs="cdn" を指定することでこれを CDN から取ってくるようになり、ファイルサイズも数十KBくらいになります。オンラインで使うなら、これは必須オプションだと思います。

これが基本ですが、埋め込み用に少しカスタマイズします。最終結果のコードだけ欲しければ このリンク を使ってください。

ファイルサイズ削減

グラフデータは HTML ファイル内の script タグに、JavaScript(JSON)として埋め込まれます。

<html>
<head>
    <meta charset="utf-8"/>
</head>
<body>
<div>
    <script type="text/javascript">window.PlotlyConfig = {MathJaxConfig: 'local'};</script>
    <script charset="utf-8" src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
    <div id="88f5c1a6-1234-4dfd-a31b-172423670750" class="plotly-graph-div" style="height:100%; width:100%;"></div>
    <script type="text/javascript">
        window.PLOTLYENV = window.PLOTLYENV || {};
        if (document.getElementById("88f5c1a6-1234-4dfd-a31b-172423670750")) {
            Plotly.newPlot("88f5c1a6-1234-4dfd-a31b-172423670750", [{
                "x": [0.0, 0.25, 0.5, 0.75, 1.0],
                "y": [0.0, 0.25, 0.5, 0.75, 1.0],
                "z": [
                    [-1.8934605001703813e-17, 150.7206, 107.16180000000001, 11.743169999999997, -0.14766200000000004],
                    [6.000000000000002, 62.860140000000015, 55.556819999999995, 98.41956000000003, 0.6423392000000008],
                    [12.000000000000005, 58.86466000000001, 319.1686, 8.04743900000001, -2.168404344971009e-19],
                    [18.0, 192.8718, 32.12764, 6.803666000000001, -0.04880907000000007],
                    [24.000000000000007, 79.56688, 123.9905, 3.3349910000000005, 3.568177],
                ],
                ...

(記事のためにフォーマットしていますが、実際には改行はありません)

"z" の値を見ると分かるように、データが float 値の場合、必要以上にファイルサイズが膨れ上がっている可能性があります。

このようなケースでは、下記のようにグラフを作成する前に値を丸めるだけでもファイルサイズを大きく削減できます。

# 適当な桁数で丸める。必要な桁数はケースバイケースなので要確認。
x = x.round(3)
y = y.round(3)
z = z.round(1)

# グラフの作成
fig = go.Figure(data=[go.Surface(x=x, y=y, z=z)])

適用前:

"z": [
    [-1.8934605001703813e-17, 150.7206, 107.16180000000001, 11.743169999999997, -0.14766200000000004],
    [6.000000000000002, 62.860140000000015, 55.556819999999995, 98.41956000000003, 0.6423392000000008],
    [12.000000000000005, 58.86466000000001, 319.1686, 8.04743900000001, -2.168404344971009e-19],
    [18.0, 192.8718, 32.12764, 6.803666000000001, -0.04880907000000007],
    [24.000000000000007, 79.56688, 123.9905, 3.3349910000000005, 3.568177],
]

適用後:

"z": [
    [-0.0, 150.7, 107.2, 11.7, -0.1],
    [6.0, 62.9, 55.6, 98.4, 0.6],
    [12.0, 58.9, 319.2, 8.0, -0.0],
    [18.0, 192.9, 32.1, 6.8, -0.0],
    [24.0, 79.6, 124.0, 3.3, 3.6],
]

他にも桁数の大きい整数値であれば 1/1000 にして軸ラベルに (K) を付けるなども有効です。ファイルサイズが 100KB を超えたらぜひ試してみてください。

グラフマージンの除去

デフォルト設定ではグラフの周りにかなり大きなマージンが生成されます。下画像のグレーの部分がすべてマージンです。

デフォルトのグラフマージン

埋め込みだとグラフエリアが小さいので、マージンは完全に除去で良いと思います。ただし、これをやるとグラフ内タイトルが見えなくなることだけ注意です。タイトルはスペースに余裕のある記事側に書けば良いかなと思います。

fig = go.Figure(
    data=[go.Surface(x=x, y=y, z=z)],
    layout=go.Layout(
        margin={"l": 0, "r": 0, "b": 0, "t": 0},  # マージンの除去。
        # paper_bgcolor="lightgray",  # 背景色
    ),
)

bodyマージンの除去

body タグにもデフォルトで 8px のマージンがあるので、これも除去します。こちらは Plotly にオプションはないので、full_html=False を指定して div タグだけを生成してもらい、自分でカスタマイズします。

div = fig.to_html(
    full_html=False,
    include_plotlyjs="cdn",
)

html = f"""\
<html>
<head><meta charset="utf-8" /></head>
<body style="margin: 0;">
{div}
</body>
</html>
"""

(任意)スクロールバーの除去

通常は必要に応じてスクロールバーの表示・非表示が切り替わりますが、ぴったりの大きさのとき、Plotly のアイコンを触ろうとすると下画像のようにバグります。

スクロールバーのチャタリング

スクロールバーが不要な時は、明示的にオフにしておいた方が良さそうです。

html = f"""\
<html>
<head><meta charset="utf-8" /></head>
<body style="margin: 0; overflow: hidden">
{div}
</body>
</html>
"""

(任意)JavaScriptの分離

この記事ではやりませんが、グラフデータを保持する JavaScript を HTML ファイルから分離して別のファイルにすることで、JavaScript だけ別のホスティングサービスを使って公開し、埋め込みサービス側のサイズ制限を回避する裏技があります。しかし、残念ながら Plotly には JavaScript だけ分離するオプションはないようです。

ということで調べたついでに分離して保存する関数を作成しました。下記コードに含まれるのでテスト用にお使いください。

サンプルコード

基本は Plotly がインストールされていれば十分ですが、下記サンプルコードはいくつか追加で依存があります。

  • NumPy, Pandas: テスト用データの作成に使っています。
  • SciPy: テスト用データの水増しに使っています。
  • Beautiful Soup: JavaScript の分離に使っています。
pip install plotly numpy pandas scipy beautifulsoup4
ここをクリックして展開

レイアウト比較

上記コードで生成した HTML ファイルを使って、各サービスの見え方を比較します。

CodePen

@[codepen](https://codepen.io/rrklimbh-the-sasster/pen/poYbwGN?default-tab=result)

デフォルト設定ではスクロールバーが表示されてしまいます。Plotly がクライアントサイズを正しく取得できないようで、明示的にグラフサイズの指定が必要です。ちなみに FireFox なら上下のバーを右クリックして「このフレーム > フレームを新しいタブで開く」で埋め込み部分だけ別タブで開けるのですが、この状態でもクライアントサイズを正しく取得できませんでした。

明示的にグラフサイズ指定してスクロールバーを消すとこんな感じ。なお、固定サイズなので、ウィンドウを狭めるとグラフ全体が表示されなくなります。

上下の UI が消せないようなので、今回の候補の中ではグラフの表示領域が一番小さいです。

ライトテーマもありますが、立体的な UI と自己主張の激しい Result ボタンのせいで Zenn との親和性は微妙な気がします。

@[codepen](https://codepen.io/rrklimbh-the-sasster/pen/yLwJXrM?default-tab=result&theme-id=light)

手動実行モードもあります。

@[codepen](https://codepen.io/rrklimbh-the-sasster/pen/preview/yLwJXrM?default-tab=result)

個別に起動時間を確認できるようにアコーディオンに入れてみました。開いたタイミングからロードが始まるので、分かりやすいかと思います。ページのロードが落ち着いてからクリックしてください。

ここをクリックして展開

JSFiddle

@[jsfiddle](https://jsfiddle.net/kenzip/esarkfp9/embedded/result)

こちらもデフォルト設定ではスクロールバーが表示されてしまいます。同様に、別タブで開いた時もクライアントサイズを正しく取得できませんでした。

明示的にグラフサイズ指定してスクロールバーを消すとこんな感じ。なお、固定サイズなので、ウィンドウを狭めるとグラフ全体が表示されなくなります。

UI が CodePen よりだいぶシンプルで、Zenn との親和性は高いと思います。外枠が表示されているので、背景が白のままでも見やすいです。

デフォルトがライトテーマですが、ダークテーマもあります。

@[jsfiddle](https://jsfiddle.net/kenzip/tnz36hv7/embedded/result/dark)

手動実行モードはありませんでした。

起動時間確認用のアコーディオンです。

ここをクリックして展開

CodeSandbox

@[codesandbox](https://codesandbox.io/embed/nlxw9j?view=preview&module=%2Findex.html&hidenavigation=1&hidedevtools=1)

デフォルト設定でもスクロールバーは表示されませんでした。別タブで開いた時も画面いっぱいに広がってくれます。ちなみに左下の Open preview in new window というボタンを押すことでも別タブで開くことができます。

今回の候補の中では唯一 iframe に height:500px が指定されており、埋め込みフレーム自体のサイズが一番大きいです。ただ、左のサイドパネルと下部のアイコンは消せないようです。右下のアイコンは割と邪魔ですね。また、右の外枠が見切れているのも気になります。

ライトテーマもあります。左のバーは少し目立たなくなりましたが、下のアイコンの主張は相変わらずです。外枠の色はほぼ白になるので、グラフ背景が白だとほぼ溶け込みます。

@[codesandbox](https://codesandbox.io/embed/nlxw9j?view=preview&module=%2Findex.html&hidenavigation=1&hidedevtools=1&theme=light)

手動実行モードもあります。ただし、クリック前の背景はライトテーマでも黒で固定のようです。

@[codesandbox](https://codesandbox.io/embed/nlxw9j?view=preview&module=%2Findex.html&hidenavigation=1&hidedevtools=1&theme=light&runonclick=1)

起動時間確認用のアコーディオンです。

ここをクリックして展開

StackBlitz

上記理由から、この記事内では手動実行モードにしていますが、デフォルトでは自動実行です。

@[stackblitz](https://stackblitz.com/edit/web-platform-gkjsfc?ctl=1&embed=1&file=index.html&hideDevTools=1&hideExplorer=1&hideNavigation=1&view=preview)

デフォルト設定でもスクロールバーは表示されませんでした。別タブで開いた時も画面いっぱいに広がってくれます。

UI が小さいため、今回の候補の中では CodeSandbox の次にグラフの表示領域が大きいです。外枠が無いので、グラフ背景が白だと完全に溶け込みます。

ライトテーマもあります。UI の主張が小さくなって良いですね。

@[stackblitz](https://stackblitz.com/edit/web-platform-gkjsfc?ctl=1&embed=1&file=index.html&hideDevTools=1&hideExplorer=1&hideNavigation=1&view=preview&theme=light)

起動時間確認用のアコーディオンです。

ここをクリックして展開

グラフエリア比較

横方向はほぼ同じなので縦方向だけ比べます。グレーの部分がグラフエリアです。

比較

データサイズ上限

各サービスでアップロードできるデータサイズ上限について調べました。なお、外部ホスティングサービスを使うことは考慮に入れてません。それが使えれば多分どこもブラウザのメモリ制限だけの問題になると思います。

また、これらはあくまで設定上の上限であり、快適に表示できる保証はありません。現実的には 1MB を超えるとブラウザではまともに編集もできないでしょう。

なお、プロジェクト数の上限はどのサービスもパブリックなら無制限という方針のようです。

CodePen

https://codepen.io/features/pro

https://blog.codepen.io/documentation/limitations/

We disable save on Pens with over 1 million characters total or 1 MB of code.

とあります。1プロジェクト辺り 1MB までですね。

JSFiddle

https://jsfiddle.net/extra/

どこにも言及が見つかりませんでした。

試しに1MB強のHTMLをアップロードしてみましたが表示できました。ユーザーのモラルが試されていますね。

CodeSandbox

https://codesandbox.io/pricing

https://codesandbox.io/docs/learn/legacy-sandboxes/file-upload#uploading-static-files

埋め込みの言及は見つかりませんでしたが、静的ファイルはトータルで20MBまで、Proユーザーなら500MBまでだそうなので、この辺でしょう。試しに1MB強のHTMLをアップロードしてみましたが表示できました。

Repositories の方ならもっと大きなストレージが使えるそうですが、グラフの埋め込みのために使うには流石に重すぎる気がします。

StackBlitz

https://stackblitz.com/pricing

Up to 1MB of file uploads per project

とあります。Personal+ プラン(TEAMSではない)で無制限にできるそうです。

ライセンス

そもそも生データを公開することになるのでパブリックライセンスになるのは必至ですが、一応各サービスがどういう思想なのか確認しておきます。

CodePen

https://blog.codepen.io/documentation/terms-of-service/

https://blog.codepen.io/documentation/licensing/

In short: public Pens are MIT Licensed, private Pens are owned by you with no implicit license. Read on for more detail.

JSFiddle

https://jsfiddle.net/terms

基本的な免責事項しか書かれていませんが、13歳未満は利用禁止 という記載がありました。

CodeSandbox

https://codesandbox.io/legal/terms

Public Sandboxes that existed prior to the effective date of these Terms are MIT licensed by default, these Terms becoming applicable do not change that situation.

As of the effective date of these Terms, Sandboxes are public by default, but it is up to you to define the license terms for Sandboxes you have control over.

StackBlitz

https://stackblitz.com/terms-of-service

基本的な免責事項しか書かれていません。

まとめ

下記のサービスで、インタラクティブなグラフの埋め込みが利用できることを確認しました。

  1. CodePen
  2. JSFiddle
  3. CodeSandbox
  4. StackBlitz

サイズや UI などは一長一短があり好みが分かれると思いますが、どれも十分に目的を達成できそうです。ただし注意点として、

  • CodePen と JSFiddle は、明示的なグラフのサイズ指定が必要
  • CodeSandbox は、画像としてダウンロードする機能が使用不可
  • CodeSandbox は、ロードに失敗することがある
  • StackBlitz は、実質的に手動実行モード必須

個人的にはお勧めするなら StackBlitz です。手動実行モードが必須なのは少し残念ですが、その他に目立った欠点がないのが良いです。

来栖川電算

Discussion