概要
Pythonのstreamlitライブラリを使ってデータ可視化webアプリを作成した
はじめに
普段、実験で出力されるのはcsvデータで、データ加工、解析、可視化もろもろはすべてExcelアプリで行っている
ここで問題なのは、csvデータが大きい(測定項目が多い、時間が長い、サンプリングが多い)と読込に時間がかかるし、グラフで表示する際に、固まってしまう。そして、ほかのExcel作業ができなくなる。
これを何とかしたいと思い、データ加工/解析はExcel関数が便利だなぁ感じるので、まずは、データ可視化部分について専用アプリが作れないか考えてみた
アプリ
ファイルのアップロード、表示ラベルの選択、グラフ操作など、すべてをマウスのみで作業が可能!
コード
streamlit_app.py
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
from plotly.colors import DEFAULT_PLOTLY_COLORS
import re
import math
import json
import io
# データ読込み_ファイルをキャッシュ対象として処理
@st.cache_data
def load_data(file_bytes):
# BytesIOに変換してから読み込む
if uploaded_file.name.endswith(".csv"):
df = pd.read_csv(io.BytesIO(file_bytes))
elif uploaded_file.name.endswith(".xlsx"):
df = pd.read_excel(io.BytesIO(file_bytes))
return df
# RGB -> HEX 変換関数
def rgb_to_hex(rgb_string):
match = re.match(r'rgb\((\d+), (\d+), (\d+)\)', rgb_string)
if match:
r, g, b = map(int, match.groups())
return f'#{r:02X}{g:02X}{b:02X}'
return '#000000' # fallback
# 範囲と間隔の設定関数
def auto_range_rough(min_val, max_val):
def round_min(val):
if val == 0:
return 0
base = 10 ** math.floor(math.log10(abs(val)))
step = base / 5
return math.floor(val / step) * step if val > 0 else math.ceil(val / step) * step
def round_max(val):
if val == 0:
return 0
base = 10 ** math.floor(math.log10(abs(val)))
step = base / 5
return math.ceil(val / step) * step if val > 0 else math.floor(val / step) * step
def rough_tick(min_val, max_val):
diff = (max_val - min_val) // 5
if diff == 0:
return 1
base = 10 ** math.floor(math.log10(diff))
return math.ceil(diff / base) * base
min_rounded = round_min(min_val)
max_rounded = round_max(max_val)
dtick = rough_tick(min_rounded, max_rounded)
return min_rounded, max_rounded, dtick
# デフォルト値を config_data から取得するヘルパー
def get_config_value(path, default):
if not config_data:
return default
for p in path:
config = config_data
try:
for key in p:
config = config[key]
return config
except (KeyError, IndexError, TypeError):
continue
return default
# グラフの描画
def draw_plotly_chart(df, chart_type, x_col, x_min, x_max, x_dtick, y_settings, width, height, n_charts, font_style):
layout = dict(
height=int(height),
width=int(width),
showlegend=True,
font=font_style,
hoversubplots="axis",
hovermode="x unified",
grid=dict(rows=n_charts, columns=1),
)
data = []
for i, (y_axis, y_cols, y_labels, colors, widths, dashes, y_min, y_max, y_dtick) in enumerate(y_settings, start=1):
for y_col, label, color, w, dash in zip(y_cols, y_labels, colors, widths, dashes):
if chart_type == "Line":
trace = go.Scatter(x=df[x_col], y=df[y_col], xaxis='x', yaxis=y_axis, mode="lines", name=y_col, line=dict(color=color, width=w, dash=dash))
elif chart_type == "Scatter":
trace = go.Scattergl(x=df[x_col], y=df[y_col], xaxis='x', yaxis=y_axis, mode="markers", name=y_col, marker=dict(color=color))
elif chart_type == "Scatter+Line":
trace = go.Scatter(x=df[x_col], y=df[y_col], xaxis='x', yaxis=y_axis, mode="lines+markers", name=y_col, line=dict(color=color, width=w, dash=dash), marker=dict(color=color))
elif chart_type == "Bar":
trace = go.Bar(x=df[x_col], y=df[y_col], xaxis='x', yaxis=y_axis, name=y_col, marker=dict(color=color))
data.append(trace)
# Y軸の設定を layout に直接追加(辞書に格納)
layout[f'yaxis{i}'] = dict(
title=y_labels[0] if y_labels else f'Y{i}',
range=[y_min, y_max],
dtick=y_dtick,
title_font=font_style,
tickfont=font_style,
)
fig = go.Figure(data=data, layout=layout)
# x軸ラベルの追加
fig.update_layout(
xaxis=dict(
title=x_col,
range=[x_min, x_max],
title_font=font_style,
tickfont=font_style,
dtick=x_dtick
),
)
# 表示
st.plotly_chart(fig, use_container_width=False)
# configの保存
def build_config(chart_type, n_charts, height, width, x_col, x_min, x_max, x_dtick, y_settings, df):
save_config = {
"chart_type": chart_type,
"n_charts": n_charts,
"height": height,
"width": width,
"x_col": x_col,
"x_min": x_min,
"x_max": x_max,
"x_dtick": x_dtick,
"charts": [
{
"y_axis": y_axis,
"y_cols": y_cols,
"label": [y_labels[0]] if y_labels else ["Y"],
"colors": colors,
"dashes": dashes,
"widths": widths,
"y_min": y_min,
"y_max": y_max,
"y_dtick": y_dtick
}
for (y_axis, y_cols, y_labels, colors, widths, dashes, y_min, y_max, y_dtick) in y_settings
],
# "data": df.to_dict(orient="list")
}
return save_config
# ページ設定
st.set_page_config(
page_title="Data Visualization",
page_icon="📈", # 好きな絵文字やアイコンに変更可能
layout="wide"
)
# 定数
font_style = dict(family="Meiryo UI", color="black")
# CSSで右上に文字を表示するスタイルを定義
st.markdown(
"""
<style>
.right-top {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
font-weight: bold;
color: grey;
}
</style>
""",
unsafe_allow_html=True
)
# 右上に表示する文字
st.markdown('<div class="right-top">Ver.0</div>', unsafe_allow_html=True)
# 初期化
df = None
config_data = None
config_loaded = False
save_config = []
with st.sidebar.expander("ファイル読込み"):
# ファイルアップロード
uploaded_file = st.file_uploader("データファイルをアップロード", type=["csv", "xlsx"])
# 設定読み込み(初期化)
config_file = st.file_uploader("設定ファイル(JSON)をアップロード", type="json")
# 設定ファイルを読込み
if config_file:
config_data = json.load(config_file)
# CSVデータが含まれていればDataFrameとして読み込む
if "data" in config_data:
try:
df = pd.DataFrame(config_data["data"])
uploaded_file = True # フラグ的に扱って分岐を通すため
except Exception as e:
st.error(f"設定ファイルに含まれるデータの読み込みに失敗しました: {e}")
if uploaded_file or ("data" in config_data if config_data else False):
try:
# ファイルを読み取り、バイナリに変換
file_bytes = uploaded_file.read()
# キャッシュ付きデータ読み込み
df = load_data(file_bytes)
except Exception as e:
pass
# --- サイドバー: グラフ設定 ---
with st.sidebar.expander("グラフ設定(全グラフ共通)"):
chart_type = st.selectbox("タイプ", ["Line", "Scatter", "Scatter+Line", "Bar"],
index=["Line", "Scatter", "Scatter+Line", "Bar"].index(
get_config_value([["chart_type"]], "Line")))
n_charts = st.number_input("グラフ数(行)", min_value=1, max_value=10,
value=get_config_value([["n_charts"]], 2))
height = int(st.number_input("高さ(px)", min_value=200, max_value=5000,
value=get_config_value([["height"]],250 * n_charts)))
width = int(st.number_input("幅(px)", min_value=200, max_value=5000,
value=get_config_value([["width"]],2000)))
# --- X軸の範囲と間隔設定(共通) ---
with st.sidebar.expander("X軸設定(全グラフ共通)"):
x_col = st.selectbox("X軸(共通)", df.columns,
index=df.columns.get_loc(get_config_value([["x_col"]], df.columns[0])))
# 自動スケール取得
auto_x_min, auto_x_max, auto_x_dtick = auto_range_rough(df[x_col].min(), df[x_col].max())
# 範囲と間隔
x_min = st.number_input("X軸最小値", value=get_config_value([["x_min"]], float(auto_x_min)))
x_max = st.number_input("X軸最大値", value=get_config_value([["x_max"]], float(auto_x_max)))
x_dtick = st.number_input("X軸tick間隔", value=get_config_value([["x_dtick"]], float(auto_x_dtick)))
# --- 各グラフの Y軸設定 ---
y_settings = []
color_index = 0
charts_config = get_config_value([["charts"]], [])
for i in range(n_charts):
chart_conf = charts_config[i] if i < len(charts_config) else {}
y_axis = f"y{i+1}"
with st.sidebar.expander(f"グラフ{i+1}"):
y_cols = st.multiselect(
f"Y軸(グラフ{i+1})", df.columns,
default=chart_conf.get("y_cols", []),key=f"y_{i}"
)
# ラベル名
label = st.text_input("ラベル名", value=", ".join(chart_conf.get("label",y_cols)), key=f"label_{i}")
# 一時グラフでY軸の自動範囲取得(最初のY軸列)
if y_cols:
auto_y_min, auto_y_max, auto_y_dtick = auto_range_rough(df[y_cols[0]].min(), df[y_cols[0]].max())
else:
auto_y_min, auto_y_max, auto_y_dtick = (0, 1, 1)
# 軸範囲と間隔(Y軸)
y_min = st.number_input(f"Y軸最小値(グラフ{i+1})", value=chart_conf.get("y_min", float(auto_y_min)), key=f"ymin_{i}")
y_max = st.number_input(f"Y軸最大値(グラフ{i+1})", value=chart_conf.get("y_max", float(auto_y_max)), key=f"ymax_{i}")
y_dtick = st.number_input(f"Y軸tick間隔(グラフ{i+1})", value=chart_conf.get("y_dtick", float(auto_y_dtick)), key=f"ytick_{i}")
colors,dashes,widths = [],[],[]
for j in range(len(y_cols)):
# --- 色の設定 ---
default_rgb = DEFAULT_PLOTLY_COLORS[(color_index + j) % len(DEFAULT_PLOTLY_COLORS)]
default_hex = rgb_to_hex(default_rgb)
color_value = chart_conf.get("colors", [default_hex] * len(y_cols))[j] if j < len(chart_conf.get("colors", [])) else default_hex
color = st.color_picker(f"色({y_cols[j]})", value=color_value, key=f"color_{i}_{j}")
colors.append(color)
# --- 線の種類の設定 ---
dash_options = ["実線", "点線", "破線", "ドット線"]
dash_map = {
"実線": "solid",
"点線": "dot",
"破線": "dash",
"ドット線": "dashdot"
}
# JSONからdashスタイル名を取得(dashのPlotly値から逆引きする)
default_dash = chart_conf.get("dashes", ["solid"] * len(y_cols))
default_dash_style_name = next(
(k for k, v in dash_map.items() if v == default_dash[j]),
"実線" # 見つからない場合のデフォルト
) if j < len(default_dash) else "実線"
dash_style = st.selectbox(
f"線の種類({y_cols[j]})", dash_options,
index=dash_options.index(default_dash_style_name),
key=f"dash_{i}_{j}"
)
dashes.append(dash_map[dash_style])
# --- 線の太さの設定 ---
default_widths = chart_conf.get("widths", [2] * len(y_cols))
chart_width = st.slider(
f"線の太さ({y_cols[j]})", min_value=1, max_value=10,
value=default_widths[j] if j < len(default_widths) else 2,
key=f"width_{i}_{j}"
)
widths.append(chart_width)
color_index += len(y_cols)
y_labels = [label] * len(y_cols)
y_settings.append((y_axis, y_cols, y_labels, colors, widths, dashes, y_min, y_max, y_dtick))
# --- グラフの描画 ---
draw_plotly_chart(df, chart_type, x_col, x_min, x_max, x_dtick, y_settings, width, height, n_charts, font_style)
# --- 設定を保存 ---
save_config = build_config(chart_type, n_charts, height, width, x_col, x_min, x_max, x_dtick, y_settings, df)
config_json_str = json.dumps(save_config, indent=2)
st.sidebar.download_button("JSONファイルを保存", data=config_json_str, file_name="chart_config.json", mime="application/json")
実行
ターミナルで以下を実行
streamlit run streamlit_app.py
終わり
streamlitを使うことで簡単にwebアプリの作成ができた。オフラインでも使えるので、会社のセキュリティ問題もない。グラフはplotlyにしたことでインタラクティブに操作できる。これはExcelではできないので使い勝手がGood!
今後はアプリを発展させて、データの加工やAI連携もできるようにしたい!