1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

streamlitを用いたデータ可視化Webアプリの作成

Posted at

概要

Pythonのstreamlitライブラリを使ってデータ可視化webアプリを作成した

はじめに

普段、実験で出力されるのはcsvデータで、データ加工、解析、可視化もろもろはすべてExcelアプリで行っている
ここで問題なのは、csvデータが大きい(測定項目が多い、時間が長い、サンプリングが多い)と読込に時間がかかるし、グラフで表示する際に、固まってしまう。そして、ほかのExcel作業ができなくなる。
これを何とかしたいと思い、データ加工/解析はExcel関数が便利だなぁ感じるので、まずは、データ可視化部分について専用アプリが作れないか考えてみた

アプリ

image.png

ファイルのアップロード、表示ラベルの選択、グラフ操作など、すべてをマウスのみで作業が可能!

コード

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連携もできるようにしたい!

1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?