【Streamlit】Vis Networkを使用したカスタムコンポーネントをJavascriptで作成する

タイトル画像

今回はStreamlitのカスタムコンポーネントをJavascriptで作成する手順を紹介します。

久しぶりの記事になります。

最近、技術的負債について考えることが多くなりました。
技術的負債(Technical Debt)とは、短期的な効率や納期遵守のために選ばれた“理想的でない設計や実装”が、将来的に保守や拡張を困難にし、追加コストを生む状態を指します。
技術的負債をゼロにすることは不可能だとは思いますが、プロジェクトに関わるすべての人が技術的負債について把握する姿勢がなければ、認識のずれが起きることを体感しています。

話が変わって、3年ほど前から名前を聞くようになったStreamlitがデータ利活用のツールとして注目され始めています。業務に関わらず触っていたのですが、Javascriptでカスタムコンポーネントを作成できることを知ってStreamlit活用の幅が広がりました。
今回は、StreamlitのカスタムコンポーネントをJavascriptで作成する手順を紹介します。

Streamlitとは

Streamlit は、Python だけで簡単に Web アプリケーションを作成できるオープンソースのフレームワークです。特に、データサイエンティストや機械学習エンジニアが、ダッシュボードやインタラクティブなデータ可視化ツールを素早く構築するために人気があります。

PythonだけでUI部分を簡単に作成できるということがおおきなメリットです。
今まではデータの前処理、解析、解析結果をPythonで処理することはできても、いざ他の人も使えるようにしたいと思ってもUI部分をHTMLとJavascriptであったり、Reactなどで作成する必要があり、時間が必要でした。

PythonだけでUI部分を簡単に作成できることで、簡単に作ったツールを共有することができるようになりました。個人的にはグラフの表示や、Pandasのデータフレームの表示が簡単にできることが嬉しいです。

Streamlitの活用例

最近、多くの企業でSteamlitを活用して現場改善やDX推進につながったというニュースを聞きます。このように現場で働いている人が簡単にデータに触れることのできる環境を構築することで対応コストの削減や、新たなインサイトの発見に繋がると考えています。DX推進などがトレンドということもあり、これからもStreamlitの活用は進んでいくと考えられます。

・NTTドコモ
https://www.snowflake.com/ja/customers/all-customers/case-study/docomo-dp/
社員1人1人が顧客について多角的に理解し、顧客の体験価値向上を目指す。

・日本航空
https://japan.zdnet.com/article/35233165/
データの抽出や加工、可視化を、現場社員自らが行える仕組みを構築した。

・Go株式会社
https://techblog.goinc.jp/entry/2024/10/07/185544
ログ解析の民主化。エラー発生時の調査をエンジニアに頼らざるを得ない問題に対して、ログ閲覧ツールを作成することで対応コストを削減。

Streamlitのカスタムコンポーネントとは

Streamlitに標準で用意されているウィジェットだけでは表現力に限界を感じる場面も少なくありません。たとえば、よりリッチなインタラクションや、既存の JavaScript ライブラリを活用したいといったケースです。こうしたニーズに応えるのが「カスタムコンポーネント」です。

カスタムコンポーネントを使えば、React や JavaScript、TypeScript で開発した UI を Streamlit に組み込むことができ、既存の Streamlit アプリの表現力を大きく広げることが可能になります。

カスタムコンポーネントをJavascriptで作成する手順

今回は、StreamlitのカスタムコンポーネントをJavascriptで作成する手順を紹介します。ReactやTypescriptで作成することもできますが、公式で紹介されいているためそちらを参照してください。

Create a Component - Streamlit Docs

作成するカスタムコンポーネント

今回作成するカスタムコンポーネントはVis Networkを使用した階層グラフです。以下のような階層グラフを表示するカスタムコンポーネントを作成します。階層グラフが表示され、クリックしたノードのデータを表示できるというものです。

作成するカスタムコンポーネント

グラフの用語(ノード、エッジなど)については説明しないので以下の記事を参考にしてください。

グラフ理論入門 | DevelopersIO

HTMLとJavascriptで作成する

まずは作成するカスタムコンポーネントをHTMLとJavascriptで作成します。
今回は生成AIに「Vis Networkを使用して階層グラフを表示するサンプルコードをHTMLとJavascriptで作成してください。ファイルは分けてください。」と指示して作成してもらいました。

作成したサンプルコードをブラウザで表示した画像が下の画像です。

HTMLとJavascriptで作成して表示

生成AIは指示するだけである程度のコードは作成できますが、詳しくみるとおかしな部分があるのでまだまだ安心はできません。指示の仕方によって結果が変わるとは言いますが、自分で見ておかしいと思った部分を修正していくことで、自分自身の知識を身につけた方が、指示の仕方を覚えるよりは先に繋がると考えています。

出力されたJavascriptファイルです。表示するデータ(ノードの定義、エッジの定義)はStreamlit側で定義する想定です。


// ノードの定義(レベル指定)
const nodes = new vis.DataSet([
  { id: 1, label: "Root", level: 0 },
  { id: 2, label: "Child A", level: 1 },
  { id: 3, label: "Child B", level: 1 },
  { id: 4, label: "Grandchild A1", level: 2 },
  { id: 5, label: "Grandchild B1", level: 2 }
]);

// エッジの定義
const edges = new vis.DataSet([
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 2, to: 4 },
  { from: 3, to: 5 }
]);

// ネットワークデータ
const data = {
  nodes: nodes,
  edges: edges
};

// オプション設定(階層レイアウト)
const options = {
  layout: {
    hierarchical: {
      enabled: true,
      direction: "UD", // 上から下
      sortMethod: "directed"
    }
  },
  physics: {
    enabled: false // 階層表示では通常オフにする
  }
};

// ネットワーク描画
const container = document.getElementById("root");
const network = new vis.Network(container, data, options);

// ノードクリック時の処理
network.on("click", function (params) {
  if (params.nodes.length > 0) {
    const nodeId = params.nodes[0];
    const nodeData = nodes.get(nodeId);
    document.getElementById("node-info").innerHTML = `
      <strong>ノードID:</strong> ${nodeData.id}<br>
      <strong>ラベル:</strong> ${nodeData.label}<br>
      <strong>レベル:</strong> ${nodeData.level}
    `;
  } else {
    document.getElementById("node-info").innerHTML = "ノードをクリックすると情報がここに表示されます。";
  }
});

カスタムコンポーネントのプロジェクトのセットアップ

今回はこちらのプロジェクトを使用させてもらいます。調べると様々な方法がありましたが、一番簡単にできたので紹介します。
https://github.com/blackary/cookiecutter-streamlit-component/

リポジトリの手順に沿って、セットアップを進めます。まずは以下コマンドをターミナルなどに入力します。

pip install cruft

次に、カスタムコンポーネントの設定を進めます。以下コマンドを入力するといろいろ聞かれるので入力していきます。カスタムコンポーネントを公開したり共有する予定がなく、ただただ自分自身で動かしたい場合は適当で良いです。プロジェクト名は「vis_network」としました。

cruft create https://github.com/blackary/cookiecutter-streamlit-component/
[1/9] author_name (Bob Smith):  *****
[2/9] author_email (bob@example.com): *****@example.com
[3/9] project_name (Streamlit Component X): vis_network
[4/9] package_name (vis_network):
[5/9] import_name (vis_network):
[6/9] description (Streamlit component that allows you to do X): Streamlit component that allows you to show vis network and to get event
[7/9] deployment_via_github_actions (y): n
[8/9] working_with_dataframes (n): n
[9/9] Select open_source_license
1 - MIT License
2 - BSD License
3 - ISC License
4 - Apache Software License 2.0
5 - GNU General Public License v3
6 - Not open source
Choose from [1/2/3/4/5/6] (1): 1

セットアップが完了すると、プロジェクトが作成されています。

カスタムコンポーネントをセットアップしたフォルダ構成

処理部分を追記していく

今回主に修正するファイルは、index.html、main.js、__init__.pyです。

index.html

必要なライブラリの読み込みなどを追記します。

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/vis-network/9.1.9/dist/dist/vis-network.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis-network/9.1.9/standalone/umd/vis-network.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

main.js

描画処理部分をonRender関数に追記します。また、Streamlitにデータを受け取る部分、渡す部分の処理を修正します。
「event.detail.args.data」でStreamlitから渡されるデータを取得できます。
「sendValue()」にStreamlitで表示したいデータを渡します。


function onRender(event) {
  // Only run the render code the first time the component is loaded.
  if (!window.rendered) {
    // You most likely want to get the data passed in like this
    // const {input1, input2, input3} = event.detail.args// networkを表示するjavascript始まり
    // オプション設定(階層レイアウト)
    const options = {
      layout: {
        hierarchical: {
          enabled: true,
          direction: "UD", // 上から下
          sortMethod: "directed"
        }
      },
      physics: {
          enabled: false // 階層表示では通常オフにする
      }
    };
    // Streamlitからデータを受け取り処理するために追記する
    // Streamlitから渡されるデータをパースする
    const received_data = JSON.parse(event.detail.args.data)
    // Datasetに変換しておく
    const nodes = new vis.DataSet(received_data.nodes)
    const edges = new vis.DataSet(received_data.edges)
    const data = {
        nodes: nodes,
        edges: edges
    };
    // ネットワーク描画
    const container = document.getElementById("root");
    const network = new vis.Network(container, data, options);

    // ノードクリック時の処理
    network.on("click", function (params) {
      if (params.nodes.length > 0) {
        const nodeId = params.nodes[0];
        const nodeData = nodes.get(nodeId);

        console.log('クリックされたノードのデータ', nodeData)
        // Streamlitにデータを返す。
        sendValue(nodeData)
    }
    // networkを表示するjavascript終わり
    // You'll most likely want to pass some data back to Python like this
    // sendValue({output1: "foo", output2: "bar"})
    window.rendered = true
  }
}

__init__.py

Streamlitでデータを表示できるように修正します。


# Create the python function that will be called
def vis_network(
  # Streamlitから渡されるデータ
  data: Optional[str] = None,
  ):
  """
    Add a descriptive docstring
  """
  component_value = _component_func(
    # Streamlitから渡されるデータ
    data = data,
    # keyを渡すことでイベント時の際レンダリングは行わない
    key="vis_network",
  )

  return component_value

Streamlitで表示してみる

Streamlitで表示するページを作成します。import部分はフォルダ構成に合わせて変更してください。私の場合は、custom_componentsというフォルダ配下に作成したカスタムコンポーネントのプロジェクトがあります。


import streamlit as st
import json
from custom_components.vis_network.src import vis_network

# カスタムコンポーネントに渡すデータを作成する

def get_data():
  nodes = [
    { "id": 1, "label": "Root", "level": 0 },
    { "id": 2, "label": "Child A", "level": 1 },
    { "id": 3, "label": "Child B", "level": 1 },
    { "id": 4, "label": "Grandchild A1", "level": 2 },
    { "id": 5, "label": "Grandchild B1", "level": 2 }
  ]

  edges = [
    { "from": 1, "to": 2 },
    { "from": 1, "to": 3 },
    { "from": 2, "to": 4 },
    { "from": 3, "to": 5 }
  ]

  data = {
    "nodes": nodes,
    "edges": edges
  }

  return data

st.set_page_config(layout="wide")
clicked_data = vis_network.vis_network(json.dumps(get_data(), default=str))
# カスタムコンポーネントから返されたデータを表示する
st.write(clicked_data)

このページを表示すると、下画像のようになっていると思います。
また各ノードをクリックすると、表示されるデータが変更されます。

作成するカスタムコンポーネント

まとめ

Streamlit はシンプルかつ強力なフレームワークですが、標準ウィジェットだけでは表現しきれない UI やインタラクションが必要になる場面もあります。そんな時、カスタムコンポーネントの仕組みを活用することで、React や JavaScript のライブラリを組み込み、自由自在に機能を拡張することができます。

例えば、可視化ライブラリ「vis-network」や地図・動画プレイヤー・音声処理など、既存のフロントエンド資産を活用して Streamlit アプリに組み込むことが可能です。

少しでも興味がある方は、ぜひ一度試してみてください。驚くほど簡単に、自分で表現したい UI を実現できます!

 

コメント

タイトルとURLをコピーしました