0
1

【Streamlit】カスタムコンポーネントの作り方

Last updated at Posted at 2024-06-02

はじめに

StreamlitはPythonだけで、Webアプリケーションが作成できるフレームワークです。
あらかじめUIのコンポーネントが用意されていますが、必要に応じてコンポーネントを自作することもできます。

今回は簡単なコンポーネントの作成からPyPIへの公開までの手順をまとめました。

今回作成したコンポーネント

よくあるアバターアイコンを表示するものです。
Streamlitには何故かない。。データアプリには不要というとこですかね。

image.png

  • PyPI

pip install streamlit-avatar
  • Github

環境構築

PythonおよびStreamlit、Node.jsが必要となります。が、それぞれ数多くの導入記事があるのでそちらにお任せします。

テンプレートのクローン

公式のテンプレートがあるのですが、このままだと色々混ざっているので今回は最小構成のプロジェクトにします。

git clone https://github.com/ppspps824/streamlit_avatar.git

コンポーネント作成

バックエンド側

__init__.py
import os

import streamlit.components.v1 as components

_RELEASE = False # デバッグ時はFalseにしておく

if not _RELEASE:
    _component_func = components.declare_component(
        "streamlit_avatar",
        url="http://localhost:3001",
    )
else:
    parent_dir = os.path.dirname(os.path.abspath(__file__))
    build_dir = os.path.join(parent_dir, "frontend/build")
    _component_func = components.declare_component("streamlit_avatar", path=build_dir)

# Pythonスクリプト内で呼び出す関数。通常通り引数もここで指定。
# defaultは初期状態での戻り値
def avatar(data_list):
    component_value = _component_func(data_list=data_list, default=None)
    return component_value

フロントエンド側

スタイリング部分でごちゃっとしてますが、基本はthis.props.args["xxx"]でバックエンドから受け取ったパラメータを利用できます。(今回はdata_listを受け取っています。)

index.tsx
import React from "react"
import ReactDOM from "react-dom"
import Avatar from "./Avatar"

ReactDOM.render(
  <React.StrictMode>
    <Avatar />
  </React.StrictMode>,
  document.getElementById("root")
)
Avatar.tsx
import {
  Streamlit,
  StreamlitComponentBase,
  withStreamlitConnection,
} from "streamlit-component-lib"
import React, { ReactNode, CSSProperties } from "react"

interface DataItem {
  title: string;
  url: string;
  size: number;
  caption: string;
  key: string;
}

interface AvatarProps {
  args: {
    data_list: DataItem[];
  };
}

class Avatar extends StreamlitComponentBase<AvatarProps> {
  public render = (): ReactNode => {
    const dataList: DataItem[] = this.props.args["data_list"] || [];

    const container_styles: CSSProperties = {
      display: "flex",
      flexDirection: "column",
    };

    return (
      <div style={container_styles}>
        {dataList.map((data, index) => (
          <AvatarComponent
            key={index}
            data={data}
            onClicked={this.onClicked}
          />
        ))}
      </div>
    )
  }

  private onClicked = (title: string, caption: string, key: string): void => {
    Streamlit.setComponentValue({ "title": title, "caption": caption, "key": key })
  }
}

interface AvatarComponentProps {
  data: DataItem;
  onClicked: (title: string, caption: string, key: string) => void;
}

class AvatarComponent extends React.Component<AvatarComponentProps> {
  render() {
    const { data, onClicked } = this.props;

    const container_styles: CSSProperties = {
      display: "flex",
      alignItems: "center",
      margin: "0.5rem"
    };
    const img_styles: CSSProperties = {
      width: data.size,
      height: data.size,
      borderRadius: "50%",
      objectFit: "cover",
      marginRight: "10px",
    };
    const text_container_styles: CSSProperties = {
      display: "flex",
      flexDirection: "column",
    };
    const title_styles: CSSProperties = {
      fontWeight: "bold",
    };
    const caption_styles: CSSProperties = {
      fontSize: "12px",
      color: "gray",
      marginBottom: 0,
    };

    return (
      <div style={container_styles} onClick={() => onClicked(data.title, data.caption, data.key)}>
        <img src={data.url} alt="avatar" style={img_styles} />
        <div style={text_container_styles}>
          <div style={title_styles}>{data.title}</div>
          <p style={caption_styles}>{data.caption}</p>
        </div>
      </div>
    )
  }
}

export default withStreamlitConnection(Avatar)


テスト

フロントエンドサーバの起動

cd frontend
npm run start

バックエンドサーバの起動

テスト用のスクリプトを作って

app.py
import streamlit as st
from streamlit_avatar import avatar

result = avatar(
    [
        {
            "url": "https://picsum.photos/id/237/300/300",
            "size": 40,
            "title": "Sam",
            "caption": "hello",
            "key": "avatar1",
        },
        {
            "url": "https://picsum.photos/id/238/300/300",
            "size": 40,
            "title": "Bob",
            "caption": "happy",
            "key": "avatar2",
        },
        {
            "url": "https://picsum.photos/id/23/300/300",
            "size": 40,
            "title": "Rick",
            "caption": "Bye",
            "key": "avatar3",
        },
    ]
)
st.write(result)

起動します。

streamlit run app.py

こんな感じで表示されるかと思います。
image.png

PyPIへ公開

こちらの記事を参考にさせていただきましたが、いくつか特有の注意点があります。

フロントエンドのビルド

cd frontend
npm build

__init__.py

以下に変更しておきます。

__init__.py
_RELEASE = True

MANIFEST.in

以下のように指定してbuildにfrontendが含まれるようにします。

MANIFEST.in
recursive-include <コンポーネント名>/frontend/build *

おわりに

コンポーネントを使って公開するのは私自身も初めてでしたが思った以上に簡単でした。
皆さんもぜひ!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1