顯示具有 Streamlit 標籤的文章。 顯示所有文章
顯示具有 Streamlit 標籤的文章。 顯示所有文章

2025年12月11日 星期四

在樹莓派 Pi 400 上執行 Streamlit 應用程式

我的 Pi 400 上的共用 Python 虛擬環境 myenv313 已配置完成, 基本上用來測試與佈署機器學習與量化投資 App. 但 Streamlit 不能在此環境中安裝, 因為它依賴的底層 C/C++ 擴充程式會破壞其他套件 (例如 Pandas), 必須在獨立的虛擬環境中安裝. 


1. 建立一個虛擬環境 streamlit_venv 安裝 Streamlit :

pi@raspberrypi:~ $ python -m venv streamlit_venv   
pi@raspberrypi:~ $ source ~/streamlit_venv/bin/activate   
(streamlit_venv) pi@raspberrypi:~ $ pip install streamlit --no-cache-dir   
Collecting streamlit
  Downloading streamlit-1.52.1-py3-none-any.whl.metadata (9.8 kB)
Collecting altair!=5.4.0,!=5.4.1,<7,>=4.0 (from streamlit)
  Downloading altair-6.0.0-py3-none-any.whl.metadata (11 kB)
Collecting blinker<2,>=1.5.0 (from streamlit)
  Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting cachetools<7,>=4.0 (from streamlit)
  Downloading cachetools-6.2.2-py3-none-any.whl.metadata (5.6 kB)
Collecting click<9,>=7.0 (from streamlit)
  Downloading click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
Collecting numpy<3,>=1.23 (from streamlit)
  Downloading numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl.metadata (62 kB)
Collecting packaging>=20 (from streamlit)
  Downloading packaging-25.0-py3-none-any.whl.metadata (3.3 kB)
Collecting pandas<3,>=1.4.0 (from streamlit)
  Downloading pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl.metadata (91 kB)
Collecting pillow<13,>=7.1.0 (from streamlit)
  Downloading pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl.metadata (8.8 kB)
Collecting protobuf<7,>=3.20 (from streamlit)
  Downloading protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl.metadata (593 bytes)
Collecting pyarrow>=7.0 (from streamlit)
  Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl.metadata (3.2 kB)
Collecting requests<3,>=2.27 (from streamlit)
  Downloading requests-2.32.5-py3-none-any.whl.metadata (4.9 kB)
Collecting tenacity<10,>=8.1.0 (from streamlit)
  Downloading tenacity-9.1.2-py3-none-any.whl.metadata (1.2 kB)
Collecting toml<2,>=0.10.1 (from streamlit)
  Downloading toml-0.10.2-py2.py3-none-any.whl.metadata (7.1 kB)
Collecting typing-extensions<5,>=4.4.0 (from streamlit)
  Downloading typing_extensions-4.15.0-py3-none-any.whl.metadata (3.3 kB)
Collecting watchdog<7,>=2.1.5 (from streamlit)
  Downloading watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl.metadata (44 kB)
Collecting gitpython!=3.1.19,<4,>=3.0.7 (from streamlit)
  Downloading gitpython-3.1.45-py3-none-any.whl.metadata (13 kB)
Collecting pydeck<1,>=0.8.0b4 (from streamlit)
  Downloading pydeck-0.9.1-py2.py3-none-any.whl.metadata (4.1 kB)
Collecting tornado!=6.5.0,<7,>=6.0.3 (from streamlit)
  Downloading tornado-6.5.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (2.8 kB)
Collecting jinja2 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting jsonschema>=3.0 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jsonschema-4.25.1-py3-none-any.whl.metadata (7.6 kB)
Collecting narwhals>=1.27.1 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading narwhals-2.13.0-py3-none-any.whl.metadata (12 kB)
Collecting gitdb<5,>=4.0.1 (from gitpython!=3.1.19,<4,>=3.0.7->streamlit)
  Downloading gitdb-4.0.12-py3-none-any.whl.metadata (1.2 kB)
Collecting smmap<6,>=3.0.1 (from gitdb<5,>=4.0.1->gitpython!=3.1.19,<4,>=3.0.7->streamlit)
  Downloading smmap-5.0.2-py3-none-any.whl.metadata (4.3 kB)
Collecting python-dateutil>=2.8.2 (from pandas<3,>=1.4.0->streamlit)
  Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl.metadata (8.4 kB)
Collecting pytz>=2020.1 (from pandas<3,>=1.4.0->streamlit)
  Downloading pytz-2025.2-py2.py3-none-any.whl.metadata (22 kB)
Collecting tzdata>=2022.7 (from pandas<3,>=1.4.0->streamlit)
  Downloading tzdata-2025.2-py2.py3-none-any.whl.metadata (1.4 kB)
Collecting charset_normalizer<4,>=2 (from requests<3,>=2.27->streamlit)
  Downloading charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (37 kB)
Collecting idna<4,>=2.5 (from requests<3,>=2.27->streamlit)
  Downloading idna-3.11-py3-none-any.whl.metadata (8.4 kB)
Collecting urllib3<3,>=1.21.1 (from requests<3,>=2.27->streamlit)
  Downloading urllib3-2.6.1-py3-none-any.whl.metadata (6.6 kB)
Collecting certifi>=2017.4.17 (from requests<3,>=2.27->streamlit)
  Downloading certifi-2025.11.12-py3-none-any.whl.metadata (2.5 kB)
Collecting MarkupSafe>=2.0 (from jinja2->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (2.7 kB)
Collecting attrs>=22.2.0 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading attrs-25.4.0-py3-none-any.whl.metadata (10 kB)
Collecting jsonschema-specifications>=2023.03.6 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl.metadata (2.9 kB)
Collecting referencing>=0.28.4 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading referencing-0.37.0-py3-none-any.whl.metadata (2.8 kB)
Collecting rpds-py>=0.7.1 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (4.1 kB)
Collecting six>=1.5 (from python-dateutil>=2.8.2->pandas<3,>=1.4.0->streamlit)
  Downloading six-1.17.0-py2.py3-none-any.whl.metadata (1.7 kB)
Downloading streamlit-1.52.1-py3-none-any.whl (9.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.0/9.0 MB 4.8 MB/s eta 0:00:00
Downloading altair-6.0.0-py3-none-any.whl (795 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 795.4/795.4 kB 5.4 MB/s eta 0:00:00
Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)
Downloading cachetools-6.2.2-py3-none-any.whl (11 kB)
Downloading click-8.3.1-py3-none-any.whl (108 kB)
Downloading gitpython-3.1.45-py3-none-any.whl (208 kB)
Downloading gitdb-4.0.12-py3-none-any.whl (62 kB)
Downloading numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl (14.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.2/14.2 MB 4.4 MB/s eta 0:00:00
Downloading pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl (11.7 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.7/11.7 MB 4.9 MB/s eta 0:00:00
Downloading pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl (6.3 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 4.0 MB/s eta 0:00:00
Downloading protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl (324 kB)
Downloading pydeck-0.9.1-py2.py3-none-any.whl (6.9 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 4.4 MB/s eta 0:00:00
Downloading requests-2.32.5-py3-none-any.whl (64 kB)
Downloading charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (147 kB)
Downloading idna-3.11-py3-none-any.whl (71 kB)
Downloading smmap-5.0.2-py3-none-any.whl (24 kB)
Downloading tenacity-9.1.2-py3-none-any.whl (28 kB)
Downloading toml-0.10.2-py2.py3-none-any.whl (16 kB)
Downloading tornado-6.5.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (445 kB)
Downloading typing_extensions-4.15.0-py3-none-any.whl (44 kB)
Downloading urllib3-2.6.1-py3-none-any.whl (131 kB)
Downloading watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl (79 kB)
Downloading certifi-2025.11.12-py3-none-any.whl (159 kB)
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
Downloading jsonschema-4.25.1-py3-none-any.whl (90 kB)
Downloading attrs-25.4.0-py3-none-any.whl (67 kB)
Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl (18 kB)
Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (24 kB)
Downloading narwhals-2.13.0-py3-none-any.whl (426 kB)
Downloading packaging-25.0-py3-none-any.whl (66 kB)
Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl (45.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.0/45.0 MB 4.6 MB/s eta 0:00:00
Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB)
Downloading pytz-2025.2-py2.py3-none-any.whl (509 kB)
Downloading referencing-0.37.0-py3-none-any.whl (26 kB)
Downloading rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (389 kB)
Downloading six-1.17.0-py2.py3-none-any.whl (11 kB)
Downloading tzdata-2025.2-py2.py3-none-any.whl (347 kB)
Installing collected packages: pytz, watchdog, urllib3, tzdata, typing-extensions, tornado, toml, tenacity, smmap, six, rpds-py, pyarrow, protobuf, pillow, packaging, numpy, narwhals, MarkupSafe, idna, click, charset_normalizer, certifi, cachetools, blinker, attrs, requests, referencing, python-dateutil, jinja2, gitdb, pydeck, pandas, jsonschema-specifications, gitpython, jsonschema, altair, streamlit
Successfully installed MarkupSafe-3.0.3 altair-6.0.0 attrs-25.4.0 blinker-1.9.0 cachetools-6.2.2 certifi-2025.11.12 charset_normalizer-3.4.4 click-8.3.1 gitdb-4.0.12 gitpython-3.1.45 idna-3.11 jinja2-3.1.6 jsonschema-4.25.1 jsonschema-specifications-2025.9.1 narwhals-2.13.0 numpy-2.3.5 packaging-25.0 pandas-2.3.3 pillow-12.0.0 protobuf-6.33.2 pyarrow-22.0.0 pydeck-0.9.1 python-dateutil-2.9.0.post0 pytz-2025.2 referencing-0.37.0 requests-2.32.5 rpds-py-0.30.0 six-1.17.0 smmap-5.0.2 streamlit-1.52.1 tenacity-9.1.2 toml-0.10.2 tornado-6.5.3 typing-extensions-4.15.0 tzdata-2025.2 urllib3-2.6.1 watchdog-6.0.0

Pi 400 有 4GB 記憶體, 跑起來就是快. 檢視 Streamlit 版本 :

(streamlit_venv) pi@raspberrypi:~ $ python   
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import streamlit as st   
>>> st.__version__   
'1.52.1'

檢視虛擬目錄下的套件 :

(streamlit_venv) pi@raspberrypi:~ $ pip list  
Package                   Version
------------------------- -----------
altair                    6.0.0
attrs                     25.4.0
blinker                   1.9.0
cachetools                6.2.2
certifi                   2025.11.12
charset-normalizer        3.4.4
click                     8.3.1
gitdb                     4.0.12
GitPython                 3.1.45
idna                      3.11
Jinja2                    3.1.6
jsonschema                4.25.1
jsonschema-specifications 2025.9.1
MarkupSafe                3.0.3
narwhals                  2.13.0
numpy                     2.3.5
packaging                 25.0
pandas                    2.3.3
pillow                    12.0.0
pip                       25.1.1
protobuf                  6.33.2
pyarrow                   22.0.0
pydeck                    0.9.1
python-dateutil           2.9.0.post0
pytz                      2025.2
referencing               0.37.0
requests                  2.32.5
rpds-py                   0.30.0
six                       1.17.0
smmap                     5.0.2
streamlit                 1.52.1
tenacity                  9.1.2
toml                      0.10.2
tornado                   6.5.3
typing_extensions         4.15.0
tzdata                    2025.2
urllib3                   2.6.1
watchdog                  6.0.0

依賴的大套件包括 Numpy, Pandas, 與 Pyarrow. 


2. 本機測試 Streamlit app :

我從王進德老師的 "Raspberry Pi 5 + AI創新實踐 : 電腦視覺與人工智慧應用指南" 書中找到一個範例來測試: 


將此程式取名 interactive_ui.py 存在 streamlit_app 資料夾下 :

# interactive_ui.py 
import streamlit as st
import datetime
from time import sleep

st.title("互動元件")

# 加入單行文字輸入框
st.subheader("text input")
name=st.text_input("Enter you name:")
st.write(f"Hello, {name}")

# 加入多行文字輸入框
message=st.text_area("Enter your message:")
st.write("Your message:")
st.write(message)

# 加入數值輸入框
st.subheader("number input")
age=st.number_input("Enter your age:", min_value=0, max_value=120)
st.write(f"Your age is {age}")

# 加入按鈕
st.subheader("Button")
if st.button('Click Me'):
    st.write("Button clicked!")

# 加入單選鈕
st.subheader("Radio and checkbox")
gender = st.radio("Select your gender:", ["Male", "Female", "Other"], 
horizontal=True)
st.write(f"You selected {gender}")

# 加入複選框
agree= st.checkbox("I agree to the terms and conditions")
if agree:
    st.write("Thank you for agreeing!")

# 加入下拉式選單
st.subheader("select box")
fruit = st.selectbox("Your favorite fruit:", ["Apple", "Banana", "Cherry"])
st.write(f"You selected {fruit}")

# 加入可複選的下拉式選單
options = st.multiselect("Your favorite colors:", ["Red", "Green", "Blue"])
st.write(f"You selected {options}")

# 加入滑桿
st.subheader("slider")
value = st.slider("Select a value:", 0, 100, 50)
st.write(f"Selected value: {value}")

# 加入值範圍滑桿
range_value = st.slider("Select a range of value:", 0, 100, (20, 80))
st.write(f"Selected range: {range_value}")

# 加入日期選擇器
st.subheader("date and time")
date = st.date_input("Select a date:", datetime.datetime.now())
st.write(f"Selected data: {date}")

# 加入時間選擇器
time = st.time_input("Select a time:", datetime.time(12,30))
st.write(f"Selected time: {time}")

# 加入進度條
st.subheader("progress and spinner")
progress_bar=st.progress(0)
for value in range(101):
    progress_bar.progress(value)
    sleep(0.1)
st.write("Done")

# 加入旋轉指示器
with st.spinner("等待中..."):
    sleep(10)
st.success('Done')

Streamlit 程式不可在 Thonny 中直接執行, 必須在終端機用 streamlit run 執行 :

(streamlit_venv) pi@raspberrypi:~/streamlit_app $ streamlit run interactive_ui.py   

  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:8501
  Network URL: http://192.168.50.74:8501

它會自動開啟 Chromium 瀏覽器展示結果網頁, 但同時也出現了一個顯示 "開啟你的設定檔時發生錯誤, 部分功能無法使用" 的視窗 : 




問 AI 得知原因很多, 對 Pi 400 最可能的原因是 Chromium 設定檔損毀, 通常是非正常關機或 Chromium 跑一半被 kill 掉造成, 解決辦法很簡單, 就是刪除目前的 Chromium 設定檔, 然後重開瀏覽器即可 :

mv ~/.config/chromium ~/.config/chromium_backup

重新執行上面的 interactive_ui.py 就會開啟 Chromium, 果然就解決了. 

關於 Streamlit 用法參考 :



3. 用 ngrok 讓外部可存取 Streamlit app  :

用 wget 指令下載 64 位元版的 ngrok : 

pi@raspberrypi:~ $ wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-arm64.tgz   
--2025-12-11 16:15:07--  https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-arm64.tgz
正在查找主機 bin.equinox.io (bin.equinox.io)... 35.71.179.82, 13.248.244.96, 99.83.220.108, ...
正在連接 bin.equinox.io (bin.equinox.io)|35.71.179.82|:443... 連上了。
已送出 HTTP 要求,正在等候回應... 200 OK
長度: 10292787 (9.8M) [application/octet-stream]
儲存到:「ngrok-v3-stable-linux-arm64.tgz」

ngrok-v3-stable-lin 100%[===================>]   9.82M   736KB/s  於 19s       

2025-12-11 16:15:27 (518 KB/s) - 已儲存 「ngrok-v3-stable-linux-arm64.tgz」 [10292787/10292787]

用 tar 解壓縮, 這會得到一個單一的執行檔 ngrok : 

pi@raspberrypi:~ $ tar -xvf ngrok-v3-stable-linux-arm64.tgz   
ngrok  

用 mv 指令將 ngrok 執行檔移動到 PATH 目錄 : 

pi@raspberrypi:~ $ sudo mv ngrok /usr/local/bin/   

檢視 ngrok 版本 : 

pi@raspberrypi:~ $ ngrok --version   
ngrok version 3.34.0

然後註冊 & 登入 ngrok 官網取得 authtoken (認證金鑰), 由於 ngrok 免費帳戶只能用在一個連線, 所以此處我改用 Gmail 註冊了第二個帳戶 (注意, 不是直接用 Gmail 帳戶) :


按 Copy 複製金鑰 : 




加入 ngrok authtoken (認證金鑰) :

pi@raspberrypi:~ $ ngrok config add-authtoken  1n4Ebyjktony1966PyZVSQ1nKz8Mn_tony1966At62W7RGhsx1L
Authtoken saved to configuration file: /home/pi/.config/ngrok/ngrok.yml  

金鑰會存入 .config/ngrok/ngrok.yml 檔案中. 

然後執行下列指令即可得到一個對外得 https 網址 :

ngrok http 本機埠號

pi@raspberrypi:~ $ ngrok http 8501 




可見 ngrok 會將上面的本機區網網址 http://192.168.50.74:8501 映射到公網網址 https://02433506593c.ngrok-free.app/ :




這樣就可以從公網存取這個本機網站了. 

好書 : Raspberry Pi 5 + AI創新實踐 : 電腦視覺與人工智慧應用指南

最近從母校借到王進德老師寫的樹莓派新書 : 


此書詳盡地介紹 Pi 5 從燒錄 OS 到各種視覺應用 (含 Webcam 與 Picamerra, 以及 OpenCV, MediaPipe 等), 最吸引我注意的是書末有 Streamlit + ChatGPT 應用. 

書中範例檔案可在 GitHub 下載 :


2025年6月27日 星期五

如何在 Hugging Face Spaces 平台佈署 Streamlit 網頁應用程式

在前一篇測試中我們利用 Streamlit 在本機建構了一個 web app 讓使用者輸入 OpenAI 金鑰與提示詞呼叫 Image API 來生圖, 參考 :


本篇旨在將此 web app 佈署至 Hugging Face Spaces 平台上, 作法參考 :


首先登入 Hugging Face Spaces 平台 : 


按右上角的 "+New Space" 鈕 : 




輸入空間名稱, 描述, 授權方式等欄位, 點選 Gradio SDK (之前有 Streamlit SDK 可選, 但最近被拿掉, 有可能是改版或維護而暫時下架, 故此處先用 Gradio SDK, 後續再修改 README.md 等設定), 按底下的 "Create Space" 鈕建立空間 : 





按右上角的 "File" 顯示檔案列表 :




點選 README.md 檔案 :




因為建立空間時點選 Gradio SDK, 故裡面的設定為 Gradio 的版本, 按 "Edit" 鈕進入編輯頁面 : 




將其中 SDK 改為 Streamlit 的版本 :

sdk: streamlit  
sdk_version: 1.35.0  
app_file: app.py  

改好後按左下角的 "Commit changes to main" 鈕提交更新 : 





更新結果頁面出現一個提示, 說 Streamlit 有更新的 v1.46.1 可用, 所以再次按 'edit' 修改版本 : 




改好後同樣按左下角的 "Commit changes to main" 鈕提交更新. 

按 main 後面的空間名稱回到檔案列表頁 :




按右上角 "+Contribute" 鈕點選彈出選單的 "Create a new file" : 




在上方文字框輸入 app.py (這是預設應用程式名稱), 然後將 web app 程式 streamlit_openai_image_api_test_1.py 貼到下方 Edit 輸入框裡面後, 按左下角的 "Commit changes to main" 鈕提交建立檔案 : 



  
同樣按 main 後面的空間名稱回到檔案列表頁, 可見 app.py 已在列表中. 因為此 web app 有用到 Hugging Face Spaces 平台沒有預先安裝的第三方套件 openai, 必須新增一個 requirements.txt 將 openai 列在裡面讓平台去安裝, 按右邊 "+Contribute" 鈕點選彈出選單的 "Create a new file" : 




在上方文字框輸入 requirements.txt, 在下方 Edit 文字框輸入 openai, 按左下角的 "Commit changes to main" 鈕提交建立檔案 : 




這樣就完成全部設定了 (Streamlit 不用列在 requirements.txt 裡面, 因為 Hugging Face Spaces 本身有預先安裝), 按 Spaces 後面的空間名稱即可執行此 web app :




結果如下 :




等 Hugging Face Spaces 平台重新將 Streamlit SDK 上架就不需要去修改 README.md 檔了. 

OpenAI API 學習筆記 : 呼叫 Image API 生圖 (三)

在前一篇測試裡, 我們使用 Gradio 作為 WebUI 工具, 在頁面中輸入金鑰與提示詞呼叫 OpenAI Image API 來生成圖像, 本篇則是要改用 Streamlit 來實作功能相同的 Web app. 

本系列全部測試筆記參考 :


關於 Streamlit 套件用法參考 : 


本篇要改寫的對象是前一篇測試中, 模型選單與尺寸選單有連動功能的 gradio-openai-image-api-test-2.py 這個 web app :

import gradio as gr
from openai import OpenAI

# 圖片尺寸選項
size_options={
    'dall-e-2': ['256x256', '512x512', '1024x1024'],
    'dall-e-3': ['1024x1024', '1024x1792', '1792x1024']
    }

# 呼叫 OpenAI API 生圖
def generate_images(prompt, api_key, **kwargs):
    client=OpenAI(api_key=api_key)
    replies=client.images.generate(prompt=prompt, **kwargs)
    urls=[item.url for item in replies.data]
    return urls  # Gallery 只能接收串列

# 執行按鈕時處理函式
def handler(api_key, prompt, model_sel, size_sel, image_count):
    if not api_key.strip():   # 處理使用者未輸入金鑰問題
        return [], '請輸入 OpenAI API Key'
    if not prompt.strip():   # 處理使用者未輸入提示詞問題
        return [], '請輸入提示詞 (prompt)'
    if model_sel == 'dall-e-2':
        size=size_sel
        n=image_count   # dall-e-2 允許生 1~10 張圖
    else:
        size=size_sel
        n=1   # dall-e-3 只允許生 1 張圖
    urls=generate_images(prompt, api_key, model=model_sel, n=n, size=size)
    msg=f'模型: {model_sel}\n尺寸: {size}\n張數: {n}'   # 輸出設定值
    return urls, msg  # 傳回生圖之網址串列給 Gallery, 設定值給狀態訊息

# 模型選單連動圖片尺寸選單 : 動態更新圖片尺寸選單
def update_size_options(selected_model):
    return gr.update(
        choices=size_options[selected_model],
        value=size_options[selected_model][0]
        )

# 使用 Blocks 語法 (才有元件連動功能)
with gr.Blocks(title='OpenAI Image API 測試') as blocks:
    gr.Markdown('## 🎨 使用 DALL·E 2 / 3 生圖')
    api_key=gr.Textbox(label='請輸入金鑰 (API key)', type='password')
    prompt=gr.TextArea(label='請輸入提示詞 (Prompt)', max_lines=10)
    model_sel=gr.Radio(
        label='選擇模型',
        choices=['dall-e-2', 'dall-e-3'],
        value='dall-e-3'
        )
    size_sel=gr.Radio(
        label='選擇圖片尺寸',
        choices=size_options['dall-e-3'],
        value=size_options['dall-e-3'][0]
        )
    image_count=gr.Slider(
        label='圖片張數 (DALL·E 2 專用)',
        minimum=1,
        maximum=10,
        step=1,
        value=1
        )
    submit_btn=gr.Button('開始生成')
    gallery=gr.Gallery(label='生成的圖片')
    status=gr.Textbox(label='狀態訊息', interactive=False) # 僅輸出
    # 連動更新圖片尺寸選單
    model_sel.change(
        fn=update_size_options,  # 呼叫自訂函式更新尺寸選單內容
        inputs=model_sel,  # 模型選單值
        outputs=size_sel   # 尺寸選單值
        )
    # 點擊按鈕觸發 handler
    submit_btn.click(
        fn=handler,
        inputs=[api_key, prompt, model_sel, size_sel, image_count],
        outputs=[gallery, status]
        )
blocks.launch()

此 Gradio 應用程式中利用 gr.update() 函式來動態更新 gr.Radio 元件的選項內容, 當選擇模型時會改變尺寸選擇器的選項. 在 Streamlit 並無類似的函式, 但可以利用 st.session_state 紀錄即時的選項狀態, 當主選單之選擇變化時重新渲染副選單之選項, 作法參考下面這篇的範例 7 :


Streamlit 版的 OpenAI Image API 生圖網頁介面程式如下 :


測試 1 : 模型與尺寸選擇器連動的 Streamlit 生圖介面 [看原始碼]

# streamlit_openai_image_api_test_1.py
import streamlit as st
from openai import OpenAI

# 生圖主函式
def generate_images(prompt, api_key, **kwargs):
    client=OpenAI(api_key=api_key)
    replies=client.images.generate(prompt=prompt, **kwargs)
    return [item.url for item in replies.data]

# UI 開始
st.set_page_config(page_title='OpenAI Image API 測試', layout='wide')
st.markdown('## 🎨 使用 DALL·E 2 / 3 生圖')
# 輸入金鑰與提示詞
api_key=st.text_input('請輸入金鑰 (API key)', type='password')
prompt=st.text_area('請輸入提示詞 (Prompt)', height=100)
# 圖片尺寸選項
size_options={
    'dall-e-2': ['256x256', '512x512', '1024x1024'],
    'dall-e-3': ['1024x1024', '1024x1792', '1792x1024']
    }
# 初始化 session 狀態
if 'model_sel' not in st.session_state:
    st.session_state.model_sel='dall-e-3'
if 'size_sel' not in st.session_state:
    st.session_state.size_sel=size_options['dall-e-3'][0]
# 模型選單(更新尺寸選單)
model=st.radio(
    '選擇模型',
    ['dall-e-2', 'dall-e-3'],
    index=['dall-e-2', 'dall-e-3'].index(st.session_state.model_sel)
    )
if model != st.session_state.model_sel:
    st.session_state.model_sel=model
    st.session_state.size_sel=size_options[model][0]
# 尺寸選單(依模型變化)
size_sel=st.radio(
    '選擇圖片尺寸',
    size_options[st.session_state.model_sel],
    index=size_options[st.session_state.model_sel].index(st.session_state.size_sel)
    )
st.session_state.size_sel=size_sel
# 圖片張數(僅限 dall-e-2)
image_count=1
if st.session_state.model_sel == 'dall-e-2':
    image_count=st.slider('圖片張數 (DALL·E 2 專用)', 1, 10, value=1)
sel_msg=f'模型: {st.session_state.model_sel} 尺寸: {st.session_state.size_sel}'
st.write(sel_msg)
# 按送出按鈕開始生成圖片
if st.button('開始生成', type='primary'):
    if not api_key.strip():  # 提醒必須輸入金鑰
        st.warning('⚠ 請輸入 OpenAI API Key')
    elif not prompt.strip():  # 提醒必須輸入提示詞
        st.warning('⚠️ 請輸入提示詞 (Prompt)')
    else:
        n=image_count if st.session_state.model_sel == 'dall-e-2' else 1
        with st.spinner('生成圖片中...'):
            try:
                urls=generate_images(prompt, api_key, model=st.session_state.model_sel, size=st.session_state.size_sel, n=n)
                st.success(f'✅ 成功產生 {len(urls)} 張圖片。')
                st.markdown(f'**模型**: {st.session_state.model_sel} **尺寸**: {st.session_state.size_sel} **張數**: {n}')
                cols=st.columns(min(len(urls), 3))
                for i, url in enumerate(urls):
                    with cols[i % len(cols)]:
                        st.image(url)
            except Exception as e:
                st.error(f'❌ 發生錯誤:{e}')

結果如下, 如果沒有輸入金鑰會出現提示訊息  :








輸入金鑰與下列提示詞 :

美麗的亞洲女性,優雅且高貴,柔和的自然光,細緻的五官,富有表情的雙眼配上自然的睫毛,光滑如瓷的肌膚,精緻的骨骼結構,自然的妝容,黑色亮麗的頭髮帶有柔和的波浪,神情寧靜,寫實風格,高解析度,專業人像攝影,淺景深,溫暖的色調,電影感燈光,傑作級品質,極致細緻。

用預設的 dall-e-3 模型生成 1024x1024 一張, 按生成鈕後 st.spinner 元件會在頁面顯示一個旋轉小圖表示正在生成圖片 :




當生成完畢時 st.spinner 元件會消失並展示生成之圖片 :




改選 dall-e-2 模型要求生成兩張 512x512 圖像 :





此 web app 我已佈署於 Hugging Face Spaces 平台 : 


2025年6月16日 星期一

Streamlit 學習筆記 : 展示 Altair 生成之圖表

終於來到這波 Streamlit 密集學習月的最後一站 : 輸出 Altair 繪製的圖片. 與 Bokeh 一樣我之前也只是有聽過這套件卻從未用過, 所以花了兩三天先做了初步的測試, 紀錄在下面三篇筆記中 :


本系列所有測試文章參考 :


Streamlit 提供 st.altair_chart() 函式來輸出 Altair 繪製的圖表, 其參數結構如下 :

st.altair_chart(figure, use_container_width=True, theme="streamlit", tooltip=True)


參數名稱 說明
chart Altair 的圖表物件 Chart(通常由 alt.Chart() 建立)
use_container_width 布林值,是否讓圖表自動填滿容器寬度,預設為 True
theme 指定要套用的主題例如 "streamlit" 或 "none"(預設)
tooltip 布林值,是否啟用互動工具提示,預設為 True


必要參數為 Altair 的圖表物件 Chart, 只要將 Chart 物件傳給 st.altair_chart() 即可將圖片顯示在 Streamlit 的網頁中. 


1. 折線圖 : 

Altair 的折線圖繪製方法是 mark_line(), 例如 :


測試 1-1 : 輸出 Altair 折線圖 (1) [看原始碼] 

# st-altair-line-test-1-1.py
import streamlit as st
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': [0, 1, 2, 3, 4, 5],    # x 軸資料
    'y': [-5, -2, 6, 12, 4, 9]  # y 軸資料
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_line(
    color='red',   # 線的顏色
    strokeWidth=4  # 線的粗細 (預設 2)    
    ).encode(
    x=alt.X('x', title='X 軸'),   # 設定 X 軸標籤
    y=alt.Y('y', title='Y 軸'),   # 設定 Y 軸標籤
    tooltip=['x', 'y']  # 資料點工具提示
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 折線圖',  # 圖片標題
    )
# 在 Streamlit 顯示圖表
st.altair_chart(chart, use_container_width=False)

此例因為指定了圖片尺寸, 所以要把 use_container_width 設為 False, 否則會放大到容器寬度, 結果如下 : 



下面範例設定線段樣式為虛線與背景色 : 


測試 1-2 : 輸出 Altair 折線圖 (2) [看原始碼] 

# st-altair-line-test-1-2.py
import streamlit as st
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': [0, 1, 2, 3, 4, 5],    # x 軸資料
    'y': [-5, -2, 6, 12, 4, 9]  # y 軸資料
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_line(
    color='rgba(0, 0, 255, 0.5)',   # 線的顏色
    strokeWidth=4  # 線的粗細 (預設 2)    
    ).encode(
    x=alt.X('x', title='X 軸'),   # 設定 X 軸標籤
    y=alt.Y('y', title='Y 軸'),   # 設定 Y 軸標籤
    tooltip=['x', 'y']  # 資料點工具提示
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 折線圖',  # 圖片標題
    ).configure_view(
    fill='lightyellow'  # 僅繪圖區背景色
    ).configure_title(
    anchor='end',  # 靠右
    fontSize=16,
    fontWeight='bold',
    color='blue',
    offset=15
    )
# 在 Streamlit 顯示圖表
st.altair_chart(chart, use_container_width=False)

結果如下 : 




2. 散布圖 : 

Altair 的散布圖繪製方法是 mark_point(), 例如 :


測試 2-1 : 輸出 Altair 散布圖 (1) [看原始碼] 

# st-altair-point-test-1.py
import streamlit as st
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': [0, 1, 2, 3, 4, 5],    # x 軸資料
    'y': [-5, -2, 6, 12, 4, 9]  # y 軸資料
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_point(
    filled=True,   # 填滿為實心圓點
    size=200,   # 平方像素面積
    color='red',  # 填滿顏色
    stroke='#0000FF',   # 邊框顏色
    strokeWidth=2,  # 邊框
    strokeOpacity=0.5  # 邊框透明度
    ).encode(
    x='x',
    y='y',
    tooltip=['x', 'y']   # 資料點工具提示
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 散布圖'  # 圖片標題
    )
# 在 Streamlit 顯示圖表
st.altair_chart(chart, use_container_width=False)

結果如下 : 



下面範例是利用分類欄位讓不同類別的資料點顯示不同之標記符號 (自動配色) : 


測試 2-2 : 輸出 Altair 散布圖 (2) [看原始碼] 

# st-altair-point-test-1.py
import streamlit as st
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': [0, 1, 2, 3, 4, 5],    # x 軸資料
    'y': [-5, -2, 6, 12, 4, 9],  # y 軸資料
    'label': ['A', 'B', 'A', 'B', 'A', 'B']  # 資料分類  
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_point(
    filled=True,   # 填滿為實心圓點
    size=200,   # 平方像素面積
    color='yellow',  # 填滿顏色
    stroke='navy'
    ).encode(
    x=alt.X('x', title='X 軸'),   # 設定 X 軸標籤
    y=alt.Y('y', title='Y 軸'),   # 設定 Y 軸標籤
    shape='label',  # 資料點的圖形分類欄位
    color='label',  # 顏色根據 label 分類
    tooltip=['x', 'y']   # 資料點工具提示
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 散布圖'  # 圖片標題
    ).configure_view(
    fill='lightyellow'  # 僅繪圖區背景色
    )
st.altair_chart(chart, use_container_width=False)

結果如下 :




3. 長條圖 : 

Altair 的長條圖繪製方法是 mark_bar(), 例如 :


測試 3-1 : 輸出 Altair 長條圖 (單組垂直) [看原始碼] 

# st-altair-bar-test-1.py
import streamlit as st
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    '月份': ['一月', '二月', '三月', '四月', '五月'],
    '營收': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar(
    color='cyan',  # 設定長條顏色
    size=30,   # 設定長條寬度 px
    stroke='navy'   # 設定邊框顏色
    ).encode(
    x=alt.X('月份', sort=['一月', '二月', '三月', '四月', '五月']),   # 列舉 X 軸順序
    y='營收',
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
st.altair_chart(chart, use_container_width=False)

結果如下 : 




在 encode() 中把 X, Y 軸顛倒就會得到水平長條圖, 例如 :


測試 3-2 : 輸出 Altair 長條圖 (單組水平) [看原始碼] 

# st-altair-bar-test-2.py
import streamlit as st
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    '月份': ['一月', '二月', '三月', '四月', '五月'],
    '營收': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar(
    color='cyan',  # 設定長條顏色
    size=30,   # 設定長條寬度 px
    stroke='navy'   # 設定邊框顏色
    ).encode(
    y=alt.Y('月份', sort=['一月', '二月', '三月', '四月', '五月']),   # 列舉 Y 軸順序
    x='營收',
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
st.altair_chart(chart, use_container_width=False)

結果如下 :




下面範例是繪製兩組資料的群組式垂直長條圖 :


測試 3-3 : 輸出 Altair 群組式長條圖 (雙組垂直) [看原始碼] 

# st-altair-bar-test-3.py
import streamlit as st
import altair as alt
import pandas as pd

# 原始資料
x=['一月', '二月', '三月', '四月', '五月']
y1=[120000, 135000, 99000, 150000, 170000]
y2=[100000, 125000, 87000, 140000, 160000]

# 將原始資料轉成長格式的 DataFrame
df=pd.DataFrame({
    '月份': x * 2,
    '營收': y1 + y2,
    '類別': ['今年'] * len(x) + ['去年'] * len(x)
    })

# 建立群組長條圖
chart=alt.Chart(df).mark_bar().encode(
    x=alt.X(
        '月份:N',
        title='月份',
        sort=['一月', '二月', '三月', '四月', '五月'],
        axis=alt.Axis(labelAngle=0) # 軸刻度文字旋轉 0 度=水平
        ),
    y=alt.Y('營收:Q', title='營收金額 (元)'),
    color=alt.Color('類別:N', title='類別'),
    xOffset='類別:N',  # 依照指定欄位群組化顯示長條
    tooltip=['月份', '類別', '營收']
    ).properties(
    width=400,
    height=300,
    title='今年與去年每月營收比較'
    )   
st.altair_chart(chart, use_container_width=False)

注意此例使用 alt.Axis(labelAngle=0) 將 X 軸標籤角度設為 0 (水平), 群組化關鍵是用 xOffset 參數設定分組欄位名稱, 結果如下 :



繪製群組式水平長條圖只要將 encode() 裡的 x, y 軸顛倒, 而且用 yOffset 參數設定 Y 軸分組欄位名稱即可 : 


測試 3-4 : 輸出 Altair 群組式長條圖 (雙組水平) [看原始碼] 

# st-altair-bar-test-3.py
import streamlit as st
import altair as alt
import pandas as pd

# 原始資料
df=pd.DataFrame({
    '月份': ['一月', '二月', '三月', '四月', '五月'],
    '今年': [120000, 135000, 99000, 150000, 170000],
    '去年': [100000, 125000, 87000, 140000, 160000]
    })

# 將原始資料轉成長格式的 DataFrame
df=df.melt(
    id_vars=['月份'],   # 保持不變的欄位 (其他欄位要被融化)
    value_name='營收',  # 融化後新的數值欄位名稱
    var_name='類別'     # 要新增的分類欄位名稱
    )

# 建立群組長條圖
chart=alt.Chart(df).mark_bar().encode(
    y=alt.Y(
        '月份:N',
        title='月份',
        sort=['一月', '二月', '三月', '四月', '五月'],
        axis=alt.Axis(labelAngle=0) # 軸刻度文字旋轉 0 度=水平
        ),
    x=alt.X('營收:Q', title='營收金額 (元)'),
    color=alt.Color('類別:N', title='類別'),
    yOffset='類別:N',  # 依照指定欄位群組化顯示長條
    tooltip=['月份', '類別', '營收']
    ).properties(
    width=400,
    height=300,
    title='今年與去年每月營收比較'
    )   
st.altair_chart(chart, use_container_width=False)

此例改用 df.melt() 將原始寬格式 DataFrame 改成長格式, 結果如下 :




OK, 歷經一個半月的測試, 終於把 Streamlit 的基本功能學完了, 未來應用時如果有發現遺漏的或新的用法再來補充. 不過接下來會把 Streamlit 用在機器學習/深度學習的再整理與測試上. 

2025年6月13日 星期五

Streamlit 學習筆記 : 展示 Bokeh 生成之圖表

由於之前沒學過 Bokeh 套件, 所以先花了幾天時間測試了 Bokeh 功能, 掌握了基本用法後才回頭來測試如何在 Streamlit 上輸出 Bokeh 繪製的圖片. 幾天研究下來發現, Bokeh 真是一款簡單易用, 功能強大且完整的視覺化套件, 用法參考下面幾篇測試 : 


本系列所有測試文章參考 :


1. 在虛擬環境安裝 Bokeh 2.4.3 : 

因為最新版的 Streamlit v1.45.0 目前只支援到 Bokeh v2.4.3 版, 如果系統安裝的 Boke 比這還新 (最新 v3.7.3) 會出現如下錯誤 : 

StreamlitAPIException: Streamlit only supports Bokeh version 2.4.3, but you have version
3.7.3 installed. Please run `pip install --force-reinstall --no-deps bokeh==2.4.3` to
install the correct version.

另外 bokeh v2.4.3 和較新版本的 numpy 不相容, 它僅支援到 Numpy v1.23, 如果系統的 Numpy 版本大於 v1.23 會出現如下錯誤 :

AttributeError: module 'numpy' has no attribute 'bool8'

所以如果要在 Streamlit 應用上輸出 Bokeh 繪製的圖片, 可以建立一個虛擬環境來安裝 Bokeh v2.4.3 與 Numpy v1.23.5 來測試 : 

D:\python\test>virtualenv venv    
created virtual environment CPython3.10.11.final.0-64 in 19312ms
  creator CPython3Windows(dest=D:\python\test\venv, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=C:\Users\tony1\AppData\Local\pypa\virtualenv)
    added seed packages: pip==23.3.1, setuptools==69.0.2, wheel==0.42.0
  activators BashActivator,BatchActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator

這樣便建好一個虛擬環境目錄 venv 了, 切換到 venv : 

D:\python\test>cd venv  

執行 Scripts 底下的 activate 命令稿來啟動虛擬環境 :

D:\python\test\venv>Scripts\activate   

前面出現 (venv) 表示已進入虛擬環境, 用 pip 安裝最新版 Streamlit : 

(venv) D:\python\test\venv>pip install streamlit   

再指定安裝 Bokeh v2.4.3 版 :

(venv) D:\python\test\venv>pip install bokeh==2.4.3   

完成後用 pip list 檢視虛擬環境下的 Python 套件列表 : 

(venv) D:\python\test\venv>pip list  
Package                   Version
------------------------- -----------
altair                    5.5.0
attrs                     25.3.0
blinker                   1.9.0
bokeh                     2.4.3
cachetools                5.5.2
certifi                   2025.4.26
charset-normalizer        3.4.2
click                     8.2.1
colorama                  0.4.6
gitdb                     4.0.12
GitPython                 3.1.44
idna                      3.10
Jinja2                    3.1.6
jsonschema                4.24.0
jsonschema-specifications 2025.4.1
MarkupSafe                3.0.2
narwhals                  1.42.1
numpy                     1.23.5
packaging                 24.2
pandas                    2.3.0
pillow                    11.2.1
pip                       23.3.1
protobuf                  6.31.1
pyarrow                   20.0.0
pydeck                    0.9.1
python-dateutil           2.9.0.post0
pytz                      2025.2
PyYAML                    6.0.2
referencing               0.36.2
requests                  2.32.4
rpds-py                   0.25.1
setuptools                69.0.2
six                       1.17.0
smmap                     5.0.2
streamlit                 1.45.1
tenacity                  9.1.2
toml                      0.10.2
tornado                   6.5.1
typing_extensions         4.14.0
tzdata                    2025.2
urllib3                   2.4.0
watchdog                  6.0.0
wheel                     0.42.0

這樣便完成測試環境配置了.


2. Bokeh 圖片輸出函式 st.bokeh_chart() : 

Streamlit 提供 st.bokeh_chart() 函式來輸出 Bokeh 繪製的圖表, 其參數結構如下 :

st.bokeh_chart(figure, use_container_width=False)

必要參數為 Bokeh 的 Figure 畫布物件, 只要將它傳給 st.bokeh_chart() 即可將圖片顯示在 Streamlit 的網頁中. 由於是透過 Streamlit 展示, 所以程式中毋須匯入 bokeh.plotting.show() 函式. 


3. 輸出 Bokeh 折線圖 : 

Bokeh 的散布圖方法為 fig.line(), 用法參考 : 


Bokeh 的資料來源可以是串列表示的 x, y 軸資料, 也可以是支援字典與 DataFrame 的 ColumnDataSource 物件. 首先來測試串列資料來源 : 


測試 1 : 資料來源為串列的折線圖 [看原始碼] 

# st-bokeh-test-1.py
import streamlit as st
from bokeh.plotting import figure

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
fig=figure(width=400, height=300, title='Bokeh 折線圖')  # 建立 figure 物件
fig.line(x, y, line_width=4, line_color='cyan')  # 繪製折線圖
# 在 Streamlit 顯示圖表
st.bokeh_chart(fig, use_container_width=False)

此例 x, y 軸資料來源為串列, 因為 Bokeh 有設定圖片尺寸為 400x300, 所以 st.bokeh_chart() 要傳入 use_container_width=False, 否則圖片會被自動放大到 Streamlit 容器大小, 結果如下 : 




資料來源也可以包裝成 Bokeh 的 ColumnDataSource 物件後再餵給繪圖方法的 source 參數, 這種方式能展現 Bokeh 強大的功能, 例如互動姓, 資料綁定, 多圖表資料共享, 以及能使用進階的轉換器等. 原始的串列資料可以打包成字典或 DataFrame 傳給 ColumnDataSource 類別的建構式. 


測試 2 : 資料來源為 ColumnDataSource 物件 (傳入字典) 的折線圖 [看原始碼] 

# st-bokeh-test-2.py
import streamlit as st
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
# 將字典轉成 ColumnDataSource 物件
source=ColumnDataSource(data=dict(x=x, y=y))   
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 折線圖')
# 繪製折線圖
fig.line(x='x', y='y', line_width=4, source=source)  
# 在 Streamlit 顯示圖表
st.bokeh_chart(fig, use_container_width=False)

此例將原始的串列放入以 'x' 與 'y' 為鍵的字典, 然後將此字典傳給 ColumnDataSource() 建構式打包成 ColumnDataSource 物件, 並將其傳給 fig.line() 的 source 參數. 注意, fig.line() 的 x 與 y 參數要設為 source 物件的 'x' 與 'y' 鍵. 結果如下 :




下面是傳入 DataFrame 的範例 :


測試 3 : 資料來源為 ColumnDataSource 物件 (DataFrame) 的折線圖 [看原始碼]   

# st-bokeh-test-3.py
import streamlit as st
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
import pandas as pd

df=pd.DataFrame({
    'x': [0, 1, 2, 3, 4, 5],    # x 軸資料
    'y': [-5, -2, 6, 12, 4, 9]  # y 軸資料
    })
# 將 DataFrame 轉成 ColumnDataSource 物件
source=ColumnDataSource(df) 
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 折線圖')  
# 繪製折線圖
fig.line(x='x', y='y', line_width=4, color='red', source=source)  
# 在 Streamlit 顯示圖表
st.bokeh_chart(fig, use_container_width=False)

此例直接將 DataFrame 傳給 ColumnDataSource(), 結果如下 :




4. 輸出 Bokeh 散布圖 : 

Bokeh 的散布圖方法為 fig.scatter(), 用法參考 : 


下面是資料來源為串列的散布圖範例 : 


測試 4 : 資料來源為串列的散布圖 [看原始碼] 

# st-bokeh-test-4.py
import streamlit as st
from bokeh.plotting import figure

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 散布圖')
# 繪製圓點散布圖
fig.scatter(x, y, marker='circle', size=10, color='red')  
# 在 Streamlit 顯示圖表
st.bokeh_chart(fig, use_container_width=False)

結果如下 : 




下面是資料來源為 ColumnDataSource 物件 (字典) 的散布圖範例 : 


測試 5 : 資料來源為 ColumnDataSource 物件 (字典) 的散布圖 [看原始碼] 

# st-bokeh-test-5.py
import streamlit as st
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource   

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
# 將字典轉成 ColumnDataSource 物件
source=ColumnDataSource(data=dict(x=x, y=y)) 
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 散布圖')
# 繪製圓點散布圖
fig.scatter(x='x', y='y', marker='square', size=10, color='navy', source=source)  
# 在 Streamlit 顯示圖表
st.bokeh_chart(fig, use_container_width=False)

此例改用方點 (marker='square') 為標記符號, 結果如下 : 




下面是資料來源為 ColumnDataSource 物件 (DataFrame) 的散布圖範例 : 


測試 6 : 資料來源為 ColumnDataSource 物件 (字典) 的散布圖 [看原始碼] 

# st-bokeh-test-6.py
import streamlit as st
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
import pandas as pd

df=pd.DataFrame({
    'x': [0, 1, 2, 3, 4, 5],    # x 軸資料
    'y': [-5, -2, 6, 12, 4, 9]  # y 軸資料
    })
# 將 DataFrame 轉成 ColumnDataSource 物件
source=ColumnDataSource(df) 
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 散布圖')
# 繪製圓點散布圖
fig.scatter(x='x', y='y', marker='star', size=10, color='purple', source=source)  
# 在 Streamlit 顯示圖表
st.bokeh_chart(fig, use_container_width=False)

結果如下 :




5. 輸出 Bokeh 直條圖 :   

下面是資料來源為串列的散布圖範例 : 


測試 7 : 資料來源為串列的長條圖 [看原始碼] 

# st-bokeh-test-7.py
import streamlit as st
from bokeh.plotting import figure

x=[1, 2, 3, 4, 5]  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
fig.yaxis.formatter.use_scientific=False  # 關閉科學記號轉換
# 繪製長條圖
fig.vbar(x, top=y, width=0.5, color='cyan', line_color='blue')  
# 在 Streamlit 顯示圖表
st.bokeh_chart(fig, use_container_width=False)

結果如下 : 




下面是資料來源為 ColumnDataSource 物件 (字典) 的長條圖範例 : 


測試 8 : 資料來源為 ColumnDataSource 物件 (字典) 的長條圖 [看原始碼] 

# st-bokeh-test-8.py
import streamlit as st
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource   

x=[1, 2, 3, 4, 5]  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 將字典轉成 ColumnDataSource 物件
source=ColumnDataSource(data=dict(x=x, y=y)) 
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
fig.yaxis.formatter.use_scientific=False  # 關閉科學記號轉換
# 繪製長條圖
fig.vbar(x='x', top='y', width=0.5, color='orange', line_color='black', source=source)  
# 在 Streamlit 顯示圖表
st.bokeh_chart(fig, use_container_width=False)

結果如下 :




下面是資料來源為 ColumnDataSource 物件 (DataFrame) 的長條圖範例 : 


測試 9 : 資料來源為 ColumnDataSource 物件 (字典) 的長條圖 [看原始碼] 

# st-bokeh-test-9.py
import streamlit as st
from bokeh.plotting import figure
from bokeh.transform import dodge   
from bokeh.models import ColumnDataSource

x=['一月', '二月', '三月', '四月', '五月']  # x 軸資料
y1=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
y2=[100000, 125000, 87000, 140000, 160000]
# 建立 ColumnDataSource
source=ColumnDataSource(data=dict(x=x, y1=y1, y2=y2))
# 建立 figure 物件
fig=figure(x_range=x, width=500, height=400, title='Bokeh 群組長條圖')
fig.yaxis.formatter.use_scientific=False  # y 軸為整數
# 繪製第一個長條圖
fig.vbar(
    x=dodge('x', -0.2, range=fig.x_range),   # 往左偏移 20%
    top='y1',
    width=0.4,
    color='navy',
    legend_label='今年營收',
    source=source
    )
# 繪製第二個長條圖
fig.vbar(
    x=dodge('x', 0.2, range=fig.x_range),   # 往右偏移 20%
    top='y2',
    width=0.4,
    color='cyan',
    legend_label='去年營收',
    source=source
    )
fig.legend.location='top_left'
# 在 Streamlit 顯示圖表
st.bokeh_chart(fig, use_container_width=False)

結果如下 :