1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

グラフ

1
Posted at

再現用マスタープロンプト:CSV/TSV/波形 グラフ・オシロ解析ツール

以下は、このデスクトップアプリ一式を ゼロから忠実に再現 するための指示書(プロンプト)です。
AIアシスタントにそのまま渡してください。前提・制約・各モジュールの詳細仕様・受け入れ基準を含みます。

0. あなたへの指示(AIへ)

あなたは熟練のPython/Qt開発者です。下記の仕様に厳密に従って、CSV/TSV/波形データの
グラフ作図とオシロスコープ風解析を行うデスクトップGUIアプリを実装してください。

  • 仕様に書かれたファイル構成・関数・定数値・UIラベル(日本語そのまま)・既定値・挙動をそのまま再現する。
  • 不明点は仕様の意図に沿って最小限の判断で補い、勝手な機能追加はしない。
  • 実装後、第8章「受け入れ基準」を自動テスト(offscreen)で検証し、全項目PASSを確認する。
  • 日本語UI。コメントも日本語で簡潔に。

1. ゴール(このアプリは何か)

  • 目的: CSV / TSV / 波形(テキスト)データを読み込み、8種のグラフ作図、Excel相当のグラフ編集、
    データのセル編集、オシロスコープ表示、ピーク/各種測定・FFT、マスク/アイ/ジッタ・プロトコル解読、
    複数ファイルの重ね描き&ファイルごとの一括画像出力までを行う、単一ウィンドウのデスクトップアプリ。
  • 利用者: 日本語ユーザー。波形・計測データの可視化と解析。

2. 技術スタックと厳守すべき制約

  • 言語: Python 3.12(3.9+)。GUI: PySide6(PyQt6でも同コードで動く)。
  • Qtの取得は必ず matplotlib 経由:
    from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets
    from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar
    これによりPySide6/PyQt6どちらでも動作する。
  • Qt6のスコープ付き列挙を徹底: 例 QtCore.Qt.CheckState.Checked /
    QtCore.Qt.ItemFlag.ItemIsUserCheckable / QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection /
    QtCore.Qt.Orientation.Vertical / QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded /
    QtWidgets.QDialogButtonBox.StandardButton.Ok 等。短縮形(QtCore.Qt.Checked)は使わない。
  • グラフは matplotlib.figure.FigureFigureCanvasQTAgg で埋め込む。Figure(figsize=(6,4.4), dpi=100) を初期値、
    ax = fig.add_subplot(111)。ナビゲーションツールバーも設置。
  • numpy 2.x 対応: np.trapznp.trapezoid にリネームされたので
    _trapz = getattr(np, "trapezoid", None) or np.trapz のように後方互換を取る。
  • 仮想環境は C:\.venv、起動は pythonw graph_app.py(コンソールなし)。
  • 文字描画の日本語化け対策(jp_font)と、Qtウィジェットの日本語(OSの日本語フォント前提)。
  • 既知の落とし穴:
    • 軸範囲は min/max の片側だけでも反映する(両方そろわないと無視は不可。0始まりの余白対策)。
    • Y未選択での描画はエラーポップアップを出さず空表示+ステータス案内にする。
    • matplotlibのスクロールevent.keyはShiftを取りこぼすので、Shift判定は
      QtWidgets.QApplication.keyboardModifiers() を優先で見る。
    • offscreen環境ではQtのフォントDBが空なので、テストのスクショ時は日本語TTFを
      QtGui.QFontDatabase.addApplicationFont() で登録する。

3. ファイル構成(プロジェクトルート直下)

ファイル 役割
graph_app.py メインGUI(class GraphApp(QtWidgets.QMainWindow))。エントリポイント main()
plotter.py 描画ロジック(系列ベース・GUI非依存)。
analysis.py ピーク検出・各種測定・FFT・スペクトル指標。
mathchan.py 数学チャンネル(系列演算)。
advanced.py マスク/アイ/ジッタ/プロトコル解読。
data_loader.py 文字コード・区切り自動判定とCSV/TSV読込。
config_io.py 設定のJSON保存/復元。
jp_font.py matplotlibへの日本語フォント自動設定。
requirements.txt / セットアップ.bat / 起動.bat 依存とセットアップ・起動。
tools/generate_*.py サンプルデータ生成(任意)。
サンプルデータ/ 同梱サンプル(任意)。

4. 画面レイアウト(3ペイン)

  • 中央 QSplitter(Horizontal) に左から:
    1. : QTabWidget1. データ / 2. オシロ/解析 / 3. 高度解析 の3タブ。最小幅220)。
    2. 中央: QSplitter(Vertical) = 上「グラフ表示(ツールバー+系列ON/OFFバー+キャンバス)」、下「データ編集(先頭100行・編集可)」。
    3. 右端: 「グラフ書式調整」パネル = 上段スクロール内にグラフ書式コントロール、下段に系列スタイル表(8列)。
  • 初期サイズ目安 setSizes([320, 720, 400])、ストレッチは中央のみ伸長。
  • ウィンドウ初期サイズ 1280×800、タイトル「CSV / TSV / 波形 グラフ・解析ツール」。

以降の各章は実装済みコードを精査した詳細仕様です。これを忠実に再現してください。


setup_io 領域 詳細仕様

対象ファイル: requirements.txt, 起動.bat, セットアップ.bat, jp_font.py, data_loader.py, config_io.py(いずれもプロジェクトルート C:\Users\motto\OneDrive\デスクトップ\グラフ 直下)。

1. 技術スタックと依存関係(requirements.txt)

依存パッケージと下限バージョン(記載順・コメントも含めて正確に):

  • pandas>=2.0
  • matplotlib>=3.7
  • PySide6>=6.5
  • numpy>=1.23
  • scipy>=1.9 (コメント: オシロ解析(ピーク検出・FFT)に使用)
  • charset-normalizer (バージョン下限指定なし。コメント: 文字コード自動判定の精度向上(任意だが推奨))

任意(コメントアウトされており既定ではインストールしない):

  • spicelib … LTspice 連携で波形データを生成する場合のみ(tools/generate_ltspice.py、LTspice 本体も別途必要)。ただしセットアップ.bat では別途 spicelib を併せてインストールしている旨が明記。

記載されている方針コメント:

  • GUI は PySide6 を想定(公式 Qt for Python・LGPL)。
  • PyQt6 でも同じコードで動作する(matplotlib の qt_compat 経由・Qt6 系)。

2. セットアップ(セットアップ.bat)

  • @echo off / cd /d "%~dp0"(バッチ自身のあるディレクトリへ移動)。
  • 仮想環境作成先は固定で C:\.venv
  • Python 実行体の決定: まず set "PY=%LocalAppData%\Programs\Python\Python312\python.exe"(= ユーザーローカルにインストールされた Python 3.12 を想定)。存在しなければ set "PY=python"(PATH 上の python にフォールバック)。
  • "%PY%" -m venv C:\.venv で venv 作成。
  • 作成検証: C:\.venv\Scripts\python.exe が存在しなければ [ERROR] Failed to create the virtual environment. を表示し、pauseexit /b 1
  • C:\.venv\Scripts\python.exe -m pip install --upgrade pip で pip を更新。
  • C:\.venv\Scripts\python.exe -m pip install -r requirements.txt spicelib … requirements.txt の全依存に加えて spicelib も明示的にインストールする。
  • 完了後 Done. Double-click 起動.bat to launch the app. を表示し pause

3. 起動(起動.bat)

  • @echo off / cd /d "%~dp0"
  • GUI 用 Python の決定: set "PYW=C:\.venv\Scripts\pythonw.exe"。存在しなければ set "PYW=pythonw"(PATH の pythonw にフォールバック)。
  • 起動コマンド: start "" "%PYW%" graph_app.py(コンソールを出さない pythonw でエントリポイント graph_app.py を起動)。

4. 日本語フォント自動設定(jp_font.py)

matplotlib に日本語フォントを設定し文字化け(□□□)を防ぐユーティリティ。

定数:

  • _CANDIDATES(優先順、上から探す):
    • Windows: "Yu Gothic", "Meiryo", "BIZ UDGothic", "MS Gothic", "MS PGothic", "Yu Mincho"
    • macOS: "Hiragino Sans", "Hiragino Kaku Gothic Pro", "Hiragino Maru Gothic Pro"
    • Linux: "Noto Sans CJK JP", "IPAexGothic", "IPAGothic", "TakaoGothic", "VL Gothic"

関数:

  • available_japanese_fonts()list[str]fm.fontManager.ttflist のフォント名集合と _CANDIDATES の積を、_CANDIDATES の優先順で返す。
  • setup_japanese_font(preferred=None)str | None
    • 探索順 order: preferred が与えられればその名前を先頭に置き、続けて _CANDIDATES を連結。
    • order を順に走査し、インストール済み集合に含まれる最初の名前を採用:
      • matplotlib.rcParams["font.family"] = "sans-serif"
      • matplotlib.rcParams["font.sans-serif"] の先頭に採用名を挿入(既存リストから同名は除外して重複回避)。
      • matplotlib.rcParams["axes.unicode_minus"] = False(マイナス記号化け防止)。
      • 採用名を返す。
    • どれも見つからない場合: axes.unicode_minus = False のみ設定して None を返す。

5. CSV/TSV 読み込み(data_loader.py)

依存: csv, os, pandas as pd

定数:

  • _ENCODINGS = ["utf-8-sig", "utf-8", "cp932", "shift_jis", "euc-jp", "latin-1"](コメント上の試行候補。ただし detect_encoding 内ではこのリストを直接ループ使用してはおらず、後述の独自手順を踏む)。
  • _DELIMITERS = [",", "\t", ";", "|"]
  • DELIMITER_LABELS(UI 表示用ラベル。正確な日本語):
    • ",""カンマ ( , )"
    • "\t""タブ ( \\t )"
    • ";""セミコロン ( ; )"
    • "|""パイプ ( | )"

関数 _japanese_score(text)(jp, bad):

  • 各文字の ord を判定。日本語判定(jp 加算): 0x3040–0x30FF(ひらがな・カタカナ), 0x4E00–0x9FFF(CJK統合漢字), 0xFF00–0xFFEF(半角・全角形)。
  • 壊れ判定(bad 加算): 私用領域 0xE000–0xF8FF、または置換文字 "�"

関数 detect_encoding(path)str(判定手順を厳密に順序通り):

  1. ファイルをバイナリ全読み(raw)。
  2. BOM 判定: b"\xef\xbb\xbf" 始まりなら "utf-8-sig"b"\xff\xfe" または b"\xfe\xff" 始まりなら "utf-16"
  3. raw.decode("utf-8") が成功すれば "utf-8-sig" を返す(UTF-8 厳格チェック。成功時は utf-8 ではなく utf-8-sig を返す点に注意)。
  4. 日本語候補 ("cp932", "euc-jp") を順に decode。先頭 100000 文字 text[:100000]_japanese_scorebad == 0 and jp > 0 を満たす中で jp 最大のものを best_jp として選び、あれば返す。
  5. charset-normalizer のヒント(from charset_normalizer import from_bytesfrom_bytes(raw).best())。日本語系のみ採用:
    • 判定対象名 jp_names = {"cp932","shift-jis","shift_jis","sjis","ms932","windows-31j","euc-jp","euc_jp","iso-2022-jp"}
    • 別名解決 alias = {"shift-jis":"cp932","shift_jis":"cp932","sjis":"cp932","ms932":"cp932","windows-31j":"cp932","euc_jp":"euc-jp"}
    • 検出名を小文字化+_-置換し、enc in jp_names または "jp" in enc または "932" in enc の場合のみ alias.get(enc, enc) を返す。import 失敗や例外は握りつぶす(except Exception: pass)。
  6. 欧文・その他フォールバック: ("cp1252", "latin-1") を順に decode、成功した最初を返す。最終的に "latin-1" を返す(必ず成功)。

関数 detect_delimiter(path, encoding)str:

  • 拡張子優先: .tsv なら無条件で "\t"
  • 先頭 8192 文字を errors="replace" で読む(読み込み失敗時は .csv なら ","、それ以外 "\t")。空サンプルも同様のフォールバック。
  • csv.Sniffer().sniff(sample, delimiters="".join(_DELIMITERS)) を試行し、dialect.delimiter_DELIMITERS に含まれればそれを返す。
  • Sniffer 失敗(csv.Error)時: 先頭行で各区切り文字の出現回数を数え(counts)、最多のもの best を採用(counts[best] > 0 の場合のみ)。0 件なら .csv","、それ以外 "\t"

関数 load_table(path, encoding=None, delimiter=None)(df, encoding, delimiter):

  • path がファイルでなければ FileNotFoundError(f"ファイルが見つかりません: {path}")
  • encoding=None なら detect_encodingdelimiter=None なら detect_delimiter で自動判定。
  • 読み込み引数: read_kwargs = dict(sep=delimiter, engine="python", skip_blank_lines=True)
  • pd.read_csv(path, encoding=encoding, **read_kwargs) を実行。
    • UnicodeDecodeError 時は encoding_errors="replace" を加えて再読込(強制読込)。
    • pd.errors.EmptyDataError 時は ValueError("ファイルが空か、データ行がありません。")
  • 列が 0 (df.shape[1] == 0) なら ValueError("列を読み取れませんでした。区切り文字を確認してください。")
  • 列名正規化: 各列名を str(c).strip()。空文字なら "列" に置換。used 集合で重複検出し、重複する場合 f"{base}.{k}"(k=1,2,…)を付けて一意化(生成名も含めて必ず一意化)。df.columns を更新。
  • 戻り値は (df, encoding, delimiter)

関数 numeric_columns(df)list[str]:

  • 各列を pd.to_numeric(df[c], errors="coerce") し、非 NaN 率 s.notna().mean() >= 0.8(8割以上が数値)なら数値列とみなす。グラフの値軸候補に使う。

6. 設定の保存/復元(config_io.py)

依存: json, os。UI 状態を 1 個の dict にまとめ JSON 保存/読込。終了時自動保存・起動時自動復元に使用。

定数:

  • APP_DIR = os.path.join(os.path.expanduser("~"), ".csv_graph_tool")(ユーザーホーム配下 ~/.csv_graph_tool)。
  • LAST_CONFIG = os.path.join(APP_DIR, "last_session.json")
  • CONFIG_VERSION = 1

関数:

  • ensure_app_dir()APP_DIRos.makedirs(APP_DIR, exist_ok=True) でフォルダ作成。
  • save_config(config, path)pathdata = dict(config) をコピーし data["_version"] = CONFIG_VERSION を付加。path の親フォルダを os.makedirs(..., exist_ok=True) で作成。json.dump(data, f, ensure_ascii=False, indent=2)(UTF-8、非ASCIIエスケープなし、インデント2)。
  • load_config(path)dict | None。UTF-8 で読み json.loadOSError / ValueError(JSON 不正含む)時は None
  • save_last_session(config)path | Noneensure_app_dir() 後に save_config(config, LAST_CONFIG)OSError 時は None
  • load_last_session()dict | NoneLAST_CONFIG がファイルとして存在すれば load_config、無ければ None

既知の制約・回避策(コード由来の注意点)

  • detect_encoding は UTF-8 厳格成功時に "utf-8" ではなく "utf-8-sig" を返す(BOM 無しでも utf-8-sig 扱い。pandas 読込では BOM があれば除去される)。
  • _ENCODINGS 定数は宣言されているが detect_encoding 本体のループには未使用(実判定は BOM→UTF-8厳格→cp932/euc-jp日本語スコア→charset-normalizer日本語限定→cp1252/latin-1 の独自手順)。
  • 日本語スコア検証(bad==0 and jp>0)により、欧文ファイルへの cp932 強制適用や euc-jp の big5 等への誤判定を回避する設計。
  • charset-normalizer 未インストールでも try/except で問題なく動作(任意依存)。
  • 列名の空白除去で重複が生じた場合 .1 等を付与し、df[col] が DataFrame 化して数値判定・描画が壊れるのを防ぐ。
  • latin-1 は任意バイト列を必ず復号できるため最終フォールバックとして使われる(文字化けの可能性はあるが例外は出ない)。

plotter(plotter.py)詳細仕様

ファイル: C:\Users\motto\OneDrive\デスクトップ\グラフ\plotter.py
モジュール概要: matplotlib Axes を受け取り、系列ベースでグラフを描画する純ロジック層。GUI から独立。依存: warnings, numpy as np, pandas as pd。冒頭 docstring に「複数ファイル由来の系列をまとめて描画」「系列ごと色・線種・線幅・マーカー」「軸範囲・対数軸・凡例位置の編集」「オシロスコープ風 div グリッド(time/div・V/div・位置オフセット)」と明記。

1. モジュール定数(正確な値)

CHART_TYPES(GUIコンボの並びと一致させること)

リスト順: ["折れ線", "棒", "横棒", "積み上げ棒", "散布図", "ヒストグラム", "箱ひげ", "円"](8種)。

CHART_INFO(キーごとに use_x / multi_y / multi_file / hint)

  • "折れ線": use_x=True, multi_y=True, multi_file=True, hint="X軸に1列、Y軸に1列以上。複数ファイルの重ね描き可"
  • "棒": use_x=True, multi_y=True, multi_file=False, hint="X軸にカテゴリ列、Y軸に1列以上(単一ファイル)"
  • "横棒": use_x=True, multi_y=True, multi_file=False, hint="X軸にカテゴリ列、Y軸に1列以上(単一ファイル)"
  • "積み上げ棒": use_x=True, multi_y=True, multi_file=False, hint="X軸にカテゴリ列、Y軸に2列以上(単一ファイル)"
  • "散布図": use_x=True, multi_y=True, multi_file=True, hint="X軸に1列、Y軸に1列以上。複数ファイル可"
  • "ヒストグラム": use_x=False, multi_y=True, multi_file=True, hint="Y軸に値の列を1列以上(分布を表示)。複数ファイル可"
  • "箱ひげ": use_x=False, multi_y=True, multi_file=True, hint="Y軸に値の列を1列以上。複数ファイル可"
  • "円": use_x=True, multi_y=False, multi_file=False, hint="X軸にラベル列、Y軸に値の列を1つ(単一ファイル)"

DEFAULT_STYLE

{"color": None, "linestyle": "-", "linewidth": 1.5, "marker": "", "markersize": 4.0, "alpha": 1.0}

コメント: color が None なら matplotlib 既定カラーサイクル。

LINESTYLES(日本語ラベル→値)

{"実線": "-", "破線": "--", "一点鎖線": "-.", "点線": ":", "なし": "None"}

MARKERS(日本語ラベル→値)

{"なし": "", "丸": "o", "四角": "s", "三角": "^", "菱形": "D", "× ": "x", "+": "+", "点": "."}
注: 「× 」のキーは末尾に半角スペースあり("× ")。「+」は全角プラス。

LEGEND_LOCS(リスト順)

["best", "upper right", "upper left", "lower left", "lower right", "right", "center left", "center right", "lower center", "upper center", "center"]

TRENDLINES(近似曲線種別、リスト順)

["なし", "線形", "多項式", "指数", "対数", "移動平均"]

SERIES_KINDS(複合グラフ系列種別、日本語→内部値)

{"自動": "", "折れ線": "line", "棒": "bar", "面": "area", "散布図": "scatter"}

SERIES_AXES(軸選択、日本語→内部値)

{"主軸": "primary", "第2軸": "secondary"}

2. ヘルパー関数

style_for(series)

DEFAULT_STYLE をコピーし series.get("style")(None なら {})で update。系列スタイル辞書を返す。

_coerce_x(values) → (values, kind)

X軸値を数値/日時/カテゴリへ判定変換。

  • pd.to_numeric(errors="coerce") の非NaN率 >= 0.8 → (float配列, "numeric")
  • そうでなければ pd.to_datetime(errors="coerce")(warnings 抑制、例外時は全 NaT)の非NaN率 >= 0.8 → (datetime配列, "datetime")
  • いずれも満たさない → (str配列, "category")

_num(values)

pd.to_numeric(pd.Series(values), errors="coerce") を float ndarray で返す。

3. fit_trendline(x, y, kind, degree=2, window=5)

戻り値: (xfit, yfit, equation, r2) または None。equation は数式文字列、r2 は決定係数(移動平均は None)。
処理: x,y を _num し有限値マスク np.isfinite(x)&np.isfinite(y) を適用。len(x) < 2 なら None。x昇順にソート(y も同順)。

  • "線形": np.polyfit(x,y,1)eq = f"y={c[0]:.4g}x{c[1]:+.4g}"
  • "多項式": deg = int(max(1, min(degree, 6)))(1〜6にクリップ)。len(x) <= deg なら None。np.polyfit(x,y,deg)。eq は各項 f"{cc:+.3g}x^{deg-i}" を連結し末尾に f"{c[-1]:+.3g}"、先頭は "y="
  • "指数"(y=a·exp(b·x)): pos = y>0pos.sum() < 2 なら None。np.polyfit(x[pos], np.log(y[pos]), 1)a=np.exp(c[1]), b=c[0]yf=a*np.exp(b*x)eq=f"y={a:.4g}·e^({b:.4g}x)"
  • "対数"(y=a·ln(x)+b): pos = x>0pos.sum() < 2 なら None。x,y を pos でフィルタ。np.polyfit(np.log(x), y, 1)yf=c[0]*np.log(x)+c[1]eq=f"y={c[0]:.4g}·ln(x){c[1]:+.4g}"
  • "移動平均": w=int(max(2, min(window, len(y))))kern=np.ones(w)/wyf=np.convolve(y,kern,mode="same")ここで即 return x, yf, f"移動平均(窓={w})", None(R²計算なし)。
  • それ以外 → None。
  • 計算中の例外は全て except Exception: return None
    R²: ss_res=Σ(y-yf)², ss_tot=Σ(y-mean(y))²r2 = 1 - ss_res/ss_totss_tot>0 のとき、それ以外は None)。

4. _data_labels(ax, xx, yy, color, fontsize, cap=40, fmt="{:.3g}")

各データ点に値ラベル注記。step = max(1, int(np.ceil(n/cap))) で間引き(最大40点目安)。有限値(x,y 両方 isfinite)のみ。ax.annotate(fmt.format(yy[i]), (xx[i],yy[i]), textcoords="offset points", xytext=(0,5), ha="center", fontsize=max(7, fontsize-1), color=color or "#333")。ラベルはY値のみを表示。

5. plot_series(本体)

シグネチャ(キーワード専用引数は * 以降):

plot_series(ax, series, chart_type, *, categories=None, bins=10, title="",
  xlabel="", ylabel="", grid=True, legend=True, legend_loc="best",
  xlim=None, ylim=None, xlog=False, ylog=False, pct=False, fonts=None,
  scope=None, markers=None, max_points=0, trendline=None,
  data_labels=False, secondary_label="")

series の形式(docstring): 折れ線/散布図={label,x,y,style}、ヒスト/箱ひげ={label,y,style}、棒/横棒/積上げ={label,y,style}(categories にXラベル配列)、円={label,y,style}(categories にラベル、y[0] 使用)。

処理順:

  1. info=CHART_INFO.get(chart_type)、None なら ValueError(f"未知のグラフ種別です: {chart_type}")series 空なら ValueError("Y軸(値)の系列を選択してください。")fonts = fonts or {}
  2. 初期化: ax.clear()_remove_twin(ax)(前回第2軸掃除)→ ax.set_aspect("auto")(円の equal 持越し回避)→ ax.set_facecolor("white")(オシロ暗背景持越し回避)→ ax.tick_params(colors="black")(目盛色既定へ)。
  3. 第2軸(折れ線/散布図のみ): いずれかの系列が axis=="secondary" なら ax2 = ax.twinx()ax._twin_secondary = ax2secondary_label があれば ax2.set_ylabel(secondary_label, fontsize=(fonts or {}).get("label",10))
  4. 種別分岐:
    • 折れ線/散布図 → _draw_xy(ax, series, line=(chart_type=="折れ線"), max_points, ax2, data_labels, trendline, fonts)
    • ヒストグラム → _draw_hist(ax, series, bins)
    • 箱ひげ → _draw_box(ax, series)
    • 棒/横棒/積み上げ棒 → categories が None なら ValueError("X軸(カテゴリ)の列を選択してください。")_draw_bar(ax, series, categories, horizontal=(=="横棒"), stacked=(=="積み上げ棒"), data_labels, fonts)
    • 円 → categories None なら ValueError("X軸(ラベル)の列を選択してください。")_draw_pie(ax, series[0], categories, pct=pct)
  5. タイトル/ラベル: title あれば set_title(fontsize=fonts.get("title",12))。chart_type が円以外なら set_xlabel(xlabel, fontsize=fonts.get("label",10))set_ylabel(ylabel or ("頻度" if ヒストグラム else ""), fontsize=...)tick_params(labelsize=fonts.get("tick",9))
  6. 対数軸・軸範囲(円以外): xlog/ylogset_xscale/set_yscale("log")xlim/ylim は片側だけでも適用xlim[0] is not Noneset_xlim(left=...)xlim[1] is not Noneset_xlim(right=...)、ylim も bottom/top で同様)。コメント: 旧仕様で両方そろわないと無視され「min=0 でも余白が残る」原因だった、を解消。
  7. オシロ表示: scope and scope.get("enabled") and chart_type in ("折れ線","散布図") のとき _apply_scope(ax, scope) を呼び grid=True に強制。
  8. 凡例/グリッド: legend かつ円以外で get_legend_handles_labels()。ax2 があれば ax2.get_legend_handles_labels() を結合(第2軸系列も凡例統合)。handles があれば ax.legend(handles, labels, loc=legend_loc, fontsize=fonts.get("tick",9))。grid かつ円以外で ax.grid(True, linestyle="--", alpha=0.4)
  9. マーカー注記: markers(リスト)の各 m で ax.plot(m["x"], m["y"], m.get("symbol","v"), color=m.get("color","red"), markersize=8)m.get("text") があれば annotate(..., xytext=(0,8), ha="center", color=m.get("color","red"), fontsize=fonts.get("tick",9))
  10. return ax

fonts 辞書のキー: title(既定12)、label(既定10)、tick(既定9)。

6. _remove_twin(ax)

getattr(ax, "_twin_secondary", None) があれば ax2.remove()(例外無視)。ax._twin_secondary = None にリセット。

7. _bar_width(xx)

数値X用の棒幅。有限値抽出後 len<2 なら 0.8。ソート差分の正値の最小×0.8、なければ 0.8。

8. _draw_xy(ax, series, line=True, max_points=0, ax2=None, data_labels=False, trendline=None, fonts=None)

  • fs = fonts.get("tick", 9)
  • 前処理: 各系列で y=_num(sr["y"])x_raw=sr.get("x") が None なら ("index", None, y, sr)。あれば _coerce_x し kind が "category" の場合は全系列共有の cat_pos/cat_order(出現順マッピング)に登録。(kind, x, y, sr)prepared に蓄積。カテゴリ位置は全系列で共有(系列ごとに目盛りを上書きして取り違える不具合の回避)。
  • 棒の本数: preparedsr.get("kind")=="bar" の数を n_bars=max(1, len)bar_idx=0
  • 各系列ループ:
    • st=style_for(sr)target = ax2 if (ax2 and sr.get("axis")=="secondary") else ax
    • skind = sr.get("kind") or ("line" if line else "scatter")
    • yerr = _num(sr.get("yerr"))(None なら None)。
    • xx 決定: index→np.arange(len(y))、category→cat_pos 参照配列、それ以外→np.asarray(x,float)
    • 間引き判定 decim: max_points 真 かつ kind != "category" かつ yerr None かつ skind in ("line","scatter") かつ len(y) > max_points
      • line → decimate_minmax(xx, y, max_points)。scatter → step=max(1, len(y)//max_points)xx[::step], y[::step]any_decimated=True
      • 非間引き時は yv=y
    • 描画分岐:
      • bar: w=_bar_width(xx)/n_barsoff=(bar_idx-(n_bars-1)/2)*wbar_idx+=1target.bar(xx+off, yv, width=w, label, color=st["color"], alpha=min(st["alpha"],0.85), yerr=yerr, capsize=3)
      • area: target.fill_between(xx,yv,color,alpha=min(st["alpha"],0.4)) + target.plot(xx,yv,label,color,linewidth,alpha)
      • scatter: target.scatter(xx,yv,label,color, s=st["markersize"]**2, marker=st["marker"] or "o", alpha)。yerr あれば target.errorbar(xx,yv,yerr[:len(yv)], fmt="none", ecolor, alpha, capsize=3)
      • line(既定): yerr あれば errorbar(..., linestyle, linewidth, marker, markersize, alpha, capsize=3)、なければ plot(..., linestyle, linewidth, marker, markersize, alpha)
    • data_labels 真 → _data_labels(target, xx, yv, st["color"], fs)
    • 近似曲線(trendline type が None/"なし" でなく、kind in ("numeric","index")、skind != "bar"): fit_trendline(xx, yv, trendline["type"], degree=trendline.get("degree",2), window=trendline.get("window",5))。fit が非Noneなら xf,yf,eq,r2。ラベル: f"{sr['label']} 近似: {eq}"r2 is not None and trendline.get("show_eq") なら " (R²={r2:.4f})" を追加、show_eq が偽なら lab=None(凡例非表示)。target.plot(xf,yf,color=st["color"] or "#444", linestyle="--", linewidth=1.3, alpha=0.9, label=lab)
  • 後処理: any_decimated なら ax._decimated = True(軸ラベルはGUI側で付与)。cat_order あれば ax.set_xticks(range(len))set_xticklabels(cat_order, rotation=45 if len>6 else 0, ha="right" if len>6 else "center")

9. _draw_hist(ax, series, bins)

各系列 y=_num、NaN除去。非空のみ data/labels/colors に蓄積。data 空なら ValueError("ヒストグラムに使える数値データがありません。")colors = colors if all(c for c in colors) else None(一部色未指定なら全て自動)。ax.hist(data, bins=int(bins), alpha=0.6, label=labels, color=colors)

10. _draw_box(ax, series)

各系列 y=_num、NaN除去、非空のみ蓄積。空なら ValueError("箱ひげ図に使える数値データがありません。")ax.boxplot(data, tick_labels=labels)、TypeError 時は ax.boxplot(data, labels=labels)(matplotlib 新旧API互換)。

11. _draw_bar(ax, series, categories, horizontal=False, stacked=False, data_labels=False, fonts=None)

  • fs=(fonts or {}).get("tick",9)labels=str配列pos=np.arange(len(labels))data=[(sr["label"], _num(sr["y"]), style_for(sr)) for sr in series]
  • 内部 _label_bars(bars, vals): data_labels 偽なら何もしない。有限値のみ。横棒→(get_width(), get_y()+get_height()/2), xytext=(3,0), va="center", ha="left"。縦棒→(get_x()+get_width()/2, get_height()), xytext=(0,3), va="bottom", ha="center"。fontsize=max(7, fs-1)、fmt=f"{v:.3g}"
  • 描画: stacked or len(data)==1 の場合は積み上げ(bottom 累積)。vals=np.nan_to_num(vals[:len(labels)])。horizontal→ax.barh(pos, vals, left=bottom, ...)、else→ax.bar(pos, vals, bottom=bottom, ...)。stacked でなければ _label_barsbottom += vals
  • それ以外(grouped): n=len(data)width=0.8/n、各 i で off=(i-(n-1)/2)*width。horizontal→barh(pos+off, vals, height=width,...)、else→bar(pos+off, vals, width=width,...)。常に _label_bars
  • 目盛: horizontal→set_yticks(pos),set_yticklabels(labels)。縦→set_xticks(pos),set_xticklabels(labels, rotation=45 if len>6 else 0, ha=...)

12. _draw_pie(ax, sr, categories, pct=False)

labels=str配列values=np.nan_to_num(_num(sr["y"]))n=min(len(labels),len(values)) で両者切詰め。mask=values>0正値のみ残す)。残り0件なら ValueError("円グラフに使える正の数値データがありません。")ax.pie(values, labels=labels, autopct="%1.1f%%" if pct else None, startangle=90, counterclock=False)ax.axis("equal")

13. _apply_scope(ax, scope)

オシロ風 div グリッド設定。scope 辞書キーと既定値:

  • x_divs(int, 既定10)、y_divs(int, 既定8)、t_per_div(float, 既定1.0)、v_per_div(float, 既定1.0)、x_pos(float, 既定0.0)、y_pos(float, 既定0.0)。
  • 範囲: x0,x1 = xc ∓ xd/2*tpdy0,y1 = yc ∓ yd/2*vpdset_xlim/set_ylim
  • 目盛: set_xticks(np.linspace(x0,x1,xd+1))set_yticks(np.linspace(y0,y1,yd+1))
  • グリッド: ax.grid(True, which="major", color="#888", linestyle="-", linewidth=0.6, alpha=0.5)
  • 暗背景: ax.set_facecolor("#0b0f0b")ax.tick_params(colors="#444", labelsize=8)
  • 情報テキスト: 左上 (0.01,0.99) transform=axes、f"{_eng(tpd)}s/div {_eng(vpd)}V/div"、va="top", ha="left", color="#7CFC00"(ライムグリーン), fontsize=9, bbox=黒 alpha=0.4 edgecolor="none"。

14. 工学表記ユーティリティ

_eng(x)(公開名 format_eng = _eng)

0 なら "0"。units リスト: [(1e-12,"p"),(1e-9,"n"),(1e-6,"µ"),(1e-3,"m"),(1,""),(1e3,"k"),(1e6,"M"),(1e9,"G")]abs(x) < factor*1000 を満たす最初の単位で f"{x/factor:.3g}{suf}"。該当なしは f"{x:.3g}"。µ はマイクロ記号 U+00B5。

_ENG_MULT

{"p":1e-12, "n":1e-9, "u":1e-6, "µ":1e-6, "m":1e-3, "k":1e3, "M":1e6, "G":1e9}("u" と "µ" 両対応。"M"=メガ大文字、"m"=ミリ小文字を区別)。

parse_eng(text, default=None)

'1ms' '500us' '2.5' '1e-3' 等を float へ。空文字は default。まず float(s) を試す(1e-3 等を確定)。失敗なら正規表現 ^\s*([+-]?[\d.]+)\s*([a-zA-Zµ]*) で数値部+単位部抽出。マッチなしや数値変換失敗で default。単位部の各文字を走査し _ENG_MULT に含む最初の接頭辞で num*倍率。接頭辞なし(例 '2V')は num をそのまま返す(倍率なし)。

eng_125_sequence(lo, hi, suffix="")

1-2-5 刻みの表示文字列リスト(オシロのプリセット用)。dec=-12 から開始、10.0**dec <= hi*1.0001 の間ループ。各 dec で m in (1,2,5) について v=m*10.0**declo*0.9999 <= v <= hi*1.0001 なら format_eng(v)+suffix を追加。dec += 1。

15. decimate_minmax(x, y, max_points)

min/max エンベロープ間引き(波形包絡を保持)。n=len(y)n<=max_points or max_points<4 ならそのまま返す。n_bins=max(1, max_points//2)edges=np.linspace(0,n,n_bins+1).astype(int)。各ビン [a,b)b<=a はスキップ。有限値インデックスから最小・最大の元位置 i_lo/i_hi を求め(全NaN時は両方 a)、i_lo<=i_hi の順序で (lo, hi2) に整列し x,y をその2点追加。結果は (np.asarray(xs), np.asarray(ys))。最終点数は概ね 2*n_bins ≈ max_points

16. 互換インターフェース

build_series_from_df(df, chart_type, x_col, y_cols)

単一 DataFrame から (series, categories) を作る。

  • 棒/横棒/積み上げ棒/円: categories=df[x_col].to_numpy()、各 y 列で {"label":c, "y":df[c].to_numpy()}
  • 折れ線/散布図: xv=df[x_col].to_numpy()、各 y 列で {"label":c, "x":xv, "y":...}
  • ヒスト/箱ひげ: {"label":c, "y":...}(categories は None)。

plot(ax, df, chart_type, x_col=None, y_cols=None, *, bins=10, title="", xlabel="", ylabel="", grid=True, legend=True, pct=False)

単一 DataFrame 簡易版(テスト・後方互換用)。info None なら ValueError(f"未知のグラフ種別です: {chart_type}")info["use_x"] and not x_col なら ValueError("X軸の列を選択してください。")y_cols 空なら ValueError("Y軸(値)の列を選択してください。")build_series_from_dfplot_series(..., xlabel=xlabel or (x_col or ""), ...) を返す。

17. 既知の制約・注意点

  • 第2軸(twinx)は折れ線/散布図のみ。ax._twin_secondary 属性に保持され次回 _remove_twin で除去。
  • 近似曲線は数値X(kind="numeric"/"index")かつ棒以外のみ。カテゴリX・日時Xでは描かれない。
  • 間引きはカテゴリ以外・誤差バー無し・線/散布のみ。max_points=0(既定)で間引き無効。間引き発生時 ax._decimated=True を立てるが軸ラベル付与はGUI側責務。
  • 棒/横棒/積み上げは複数ファイル非対応(multi_file=False)。円は単一系列(multi_y=False, series[0] のみ)。
  • 円グラフは正値のみ採用。0/負値は除外。
  • xlim/ylim は片側のみの指定でも反映(タプルの None でない要素を個別適用)。
  • ヒストグラムは一部系列のみ色指定だと全色を None(自動)に倒す。

analysis 領域 詳細仕様(analysis.py

GUI から独立した解析モジュール。入力は (時間 t[s], 信号 y) の 1 次元配列。全関数 np.asarray(..., dtype=float) で受ける。

共通・依存

  • 先頭で import numpy as np
  • trapz→trapezoid 吸収: _trapz = getattr(np, "trapezoid", None) or np.trapz(numpy 2.x の np.trapezoid、無ければ np.trapz)。
  • scipy 可否フラグ: from scipy.signal import find_peaks を try。成功で _HAVE_SCIPY = Trueexcept ExceptionFalse(簡易ピーク検出にフォールバック)。
  • 多くの scipy 利用関数は関数内で遅延 import し、失敗時はフォールバックする方針。

sampling_rate(t)

  • t.size < 2None
  • dt = np.median(np.diff(t))return 1.0/dt if dt > 0 else None

ピーク検出フォールバック _simple_peaks(sig, distance=1)

  • idx = np.where((sig[1:-1] > sig[:-2]) & (sig[1:-1] >= sig[2:]))[0] + 1 の素朴な極大検出(distance は受け取るが未使用)。

smooth_signal(y, window)

  • w = int(window)w < 3 または w > y.size → そのまま y を返す。
  • w が偶数なら w += 1(奇数化)。
  • Savitzky-Golay 優先: from scipy.signal import savgol_filtersavgol_filter(y, w, min(2, w-1))(多項式次数 = 2 と w-1 の小さい方)。
  • 失敗時: 移動平均 k = np.ones(w)/wnp.convolve(y, k, mode="same")

find_signal_peaks(y, t=None, n=5, prominence_frac=0.05, distance=None, mode="max", smooth=0)

  • 戻り値: list of dict {rank, index, time, value, prominence}(rank は 1 始まり)。
  • y.size < 3[]
  • smooth and smooth >= 3 のとき y = smooth_signal(y, smooth)(平滑化後の信号でピーク評価)。
  • sig = y if mode=="max" else -ymode="min" で谷検出)。
  • span = nanmax(y) - nanmin(y)prom = prominence_frac*span if span>0 else None
  • scipy 有り: find_peaks(sig, **kwargs)prom があれば prominence=promdistance があれば distance=int(distance)proms = props.get("prominences")
  • scipy 無し: idx = _simple_peaks(sig, distance or 1)proms = None
  • len(idx)==0[]
  • 並べ替え: proms が有れば prominence 降順 argsort(proms)[::-1]、無ければ sig[idx] 値の降順で並べ proms を全 NaN 充填。
  • 上位 n 件を採用。各要素: index=int(i), time=float(t[i]) if t is not None else None, value=float(y[i]), prominence=float(pr) if pr==pr else None(NaN 判定で None)。

周波数推定

  • _zero_crossing_period(t, y): 有限値 < 3 → None。平均を引いて `signbit` の符号反転位置を線形補間でゼロ交差時刻 `tc` 化。`tc.size<3`→None。隣接差 `half`(>0 のみ)の **median を 2 倍** が周期。`period>0` のみ返す。
  • dominant_frequency(t, y): fft_spectrum を使い DC(mag[0])を除いた mag[1:] の最大位置の周波数を返す。空・全 NaN・nanmax(m)<=0(平坦/定数) は None。返値 freqs[argnanmax+1]

立上り/立下り _edge_time(t, y, rising=True, lo=0.1, hi=0.9)

  • 有限値 < 3 → None。
  • base = nanpercentile(y, 5), top = nanpercentile(y, 95), span=top-basespan 非有限 or <=0 → None。
  • y_lo = base + lo*span, y_hi = base + hi*span(10%/90% 既定)。
  • up_cross(level): below=y<levelbelow[:-1] & ~below[1:]down_cross(level): above=y>levelabove[:-1] & ~above[1:]
  • rising: 最初の y_lo 上昇交差 i_lo の後で y_hi を上昇交差する最初の点との時間差。falling: 最初の y_hi 下降交差 i_hi の後で y_lo を下降交差する最初の点との時間差。同一エッジ上で対応付け(周期信号でも立下りを算出可能)。該当無し → None。

FFT 窓

  • WINDOWS = ["hann", "hamming", "blackman", "flattop", "rect"](UI 選択肢の順序)。
  • _window(name, n): 既定 "hann"name None→hann)。hammingnp.hamming, blackmannp.blackman, flattopscipy.signal.windows.flattop(失敗時 np.hanning), none/rect/rectangularnp.ones(n), それ以外→np.hanning(n)
    • 注意: "hann" は分岐に無く np.hanning(n) の既定経路で処理。

to_db(amp, ref=1.0, floor_db=-200.0)

  • 20*log10(amp/ref)amp<=0 の要素は floor_db。最後に np.maximum(out, floor_db)

fft_spectrum(t, y, window="hann", detrend=True)

  • 戻り (freqs[Hz], amplitude)n<4 または fs 無し → (None, None)
  • detrend で平均除去(y - mean(y)、False なら y.copy())。
  • 窓掛け yw*wspec = np.fft.rfft(yw)freqs = np.fft.rfftfreq(n, d=1/fs)
  • 片側振幅 = |spec| / (sum(w)/2)(窓のコヒーレントゲイン正規化)。

find_spectral_peaks(t, y, n=5, prominence_frac=0.02)

  • fft_spectrumamp[1:]/freqs[1:](DC 除外)に find_signal_peaks(mode="max") を適用。
  • 戻り値: list of dict {rank, frequency, amplitude}(内部 dict の timefrequencyvalueamplitude に詰め替え)。

measurements(t, y) — 戻り値: list of {name, value, unit}この順序で append

  1. 最大値 Vmax, V — nanmax(y)(有限値無ければ全項 None)
  2. 最小値 Vmin, V — nanmin(y)
  3. P-P値 Vpp, V — vmax-vmin
  4. 平均 Vmean, V — nanmean(y)
  5. 実効値 Vrms, V — sqrt(nanmean(y**2))
  6. 標準偏差 σ, V — nanstd(y)
  7. 周期, s — _zero_crossing_period
  8. 周波数(ゼロ交差), Hz — 1/period
  9. 周波数(FFT), Hz — dominant_frequency
  10. Top, V — histogram_top_base の top
  11. Base, V — base
  12. 振幅 Vamp(Top-Base), V — top-base
  13. オーバーシュート, % — amp>0 のとき (vmax-top)/amp*100、それ以外 None
  14. アンダーシュート, % — (base-vmin)/amp*100、それ以外 None
  15. 立上り時間 (10-90%), s — _edge_time(rising=True)
  16. 立下り時間 (90-10%), s — _edge_time(rising=False)
  17. +パルス幅, s — pulse_metrics.pos_width
  18. -パルス幅, s — neg_width
  19. +デューティ比, % — pos_duty
  20. -デューティ比, % — neg_duty
  21. 立上りエッジ数, (無単位)— rising_edges
  22. サイクル数, (無単位)— cycles
  23. 立上り→立上り, s — edge_intervals.rise_to_rise
  24. 立下り→立下り, s — fall_to_fall
  25. 立上り→立下り(High幅), s — rise_to_fall
  26. 立下り→立上り(Low幅), s — fall_to_rise
  27. Time@Max, s — t[nanargmax(y)](有限値無ければ None)
  28. Time@Min, s — t[nanargmin(y)]
  29. 面積 ∫y dt, V·s — t,y ともに有限なマスク fin_trapz(y[fin], t[fin])(有限点 < 2 で None)
  30. サンプル数, 点 — float(y.size)
  31. サンプリング周波数, Hz — sampling_rate(t)

edge_intervals(t, y, level=None) — 戻り dict(条件成立した key のみ)

  • 有限値 < 4 → {}
  • level 未指定なら histogram_top_base(top+base)/2top is None or top<=base{}
  • _mid_crossings で上昇 up/下降 dn を取得し、各交差を線形補間して交差時刻 ut,dt_ 化。
  • rise_to_rise = mean(diff(ut))ut.size>=2), fall_to_fall = mean(diff(dt_))dt_.size>=2)。
  • rise_to_fall(High幅) = 各上昇後の最初の下降との差の平均。fall_to_rise(Low幅) = 各下降後の最初の上昇との差の平均。

histogram_top_base(y, bins=256)

  • 有限値抽出後 size<4(None, None)vmax<=vmin(vmin, vmin)
  • np.histogram(y, bins=256)、ビン中心 centersmid=(vmin+vmax)/2
  • centers>=mid 側の最頻ビン中心 = Top(該当無し時 vmax)、centers<mid 側 = Base(無し時 vmin)。

_mid_crossings(t, y, level)

  • below=y<level。上昇 up = below[:-1] & ~below[1:]、下降 down = ~below[:-1] & below[1:] のインデックス。

pulse_metrics(t, y, level=None) — 戻り dict キー

  • 有限値 < 4 → {}level 未指定は中央しきい (top+base)/2top<=base{})。
  • rising_edges = float(len(up))
  • High 区間(各 up→次 down)の幅平均 pos_width、Low 区間(各 down→次 up)の幅平均 neg_width(無ければ None)。
  • pw and nw のとき pos_duty = pw/(pw+nw)*100, neg_duty = nw/(pw+nw)*100
  • cycles = min(len(up),len(down))(両方非ゼロ時)、片方ゼロなら len(up)

cycle_measurements(t, y) — 戻り dict(各値 np.asarray(float)

  • y0 = y - nanmean(y)、上昇ゼロ交差 up 間でサイクル分割。
  • 各サイクル [a,b)period=t[b]-t[a]>0 のみ): cycle_time=(t[a]+t[b])/2, freq=1/period, vpp=seg.max()-seg.min(), amp=vpp/2
  • キー: cycle_time, freq, amp, vpp

measurement_stats(values)

  • 有限値抽出。空 → {"min":None,"max":None,"mean":None,"std":None,"count":0}
  • それ以外 min/max/mean/std(float) と count(int)。

phase_delay(t, y1, y2) — 符号規約

  • 戻り (delay[s], phase[deg])n=min(size)<4(None, None)
  • 両信号を [:n] に切り、平均除去・nan_to_num
  • corr = np.correlate(a, b, mode="full")lag = argmax(corr) - (n-1)
  • dt = median(diff(t[:n]))delay = -lag*dt(y2 が遅れていれば正)
  • period = _zero_crossing_period(t, a)phase = -delay/period*360(遅れは負位相)
  • phase は ((phase+180)%360)-180-180..180 に正規化。period 無しは phase=None。

spectrum_metrics(t, y, n_harm=6, window="hann", half_bins=3) — 戻り dict

  • n<16{}fs 無し → {}
  • yw=(y-mean(y))*wspec=rfftpower=|spec|**2power.size<4 or 全非有限 → {}
  • 基本波ビン k0 = argmax(power[1:])+1(DC除く)、f0 = k0*fs/n
  • band(kc): [max(1,kc-half_bins), min(size,kc+half_bins+1))(基本波/高調波を近傍 ±half_bins ビンで合算しリーク対策)。
  • p_fund = power[fa:fb].sum()<=0{})。
  • 高調波: h=2..n_harm、kc=round(k0*h)kc>=size-1 で break、harm_power 加算。
  • total = power[1:].sum()(DC除く全パワー)、noise_only = max(total-p_fund-harm_power, 1e-30)nd = max(total-p_fund, 1e-30)(ノイズ+歪み)。
  • 指標式:
    • THD_pct = sqrt(harm_power/p_fund)*100
    • THD_dB = 10*log10(harm_power/p_fund)harm_power>0 のみ、それ以外 None)
    • SNR_dB = 10*log10(p_fund/noise_only)
    • SINAD_dB = 10*log10(p_fund/nd)
    • ENOB_bits = (sinad - 1.76) / 6.02
    • SFDR_dB = 10*log10(p_fund/spur.max())(spur = power のコピーで spur[0]=0, spur[fa:fb]=0spur.max()>0 のみ、それ以外 None)
  • 戻りキー: f0, THD_pct, THD_dB, SNR_dB, SINAD_dB, ENOB_bits, SFDR_dB

spectrogram(t, y, nperseg=256, window="hann")

  • fs 無し → (None,None,None)scipy.signal.spectrogram import 失敗 → (None,None,None)
  • y = nan_to_num(y - nanmean(y))nperseg = min(nperseg, len(y))nperseg<16(None,None,None)
  • _spec(y, fs=fs, window=window, nperseg=nperseg, noverlap=nperseg//2, scaling="spectrum")
  • Sxx_db = 10*log10(Sxx + 1e-20)戻り (f, tt + t[0], Sxx_db)(時間軸に開始時刻 t[0] を加算、len(t) 無しは +0.0)。

analyze(t, y, n_peaks=5, smooth=0) — まとめ便利関数。戻り dict:

  • peaks: find_signal_peaks(y, t=t, n=n_peaks, smooth=smooth)
  • troughs: find_signal_peaks(..., mode="min", smooth=smooth)
  • measurements: measurements(t, y)
  • spectral_peaks: find_spectral_peaks(t, y, n=n_peaks)

既知の制約/回避策

  • scipy 不在時: ピーク検出は _simple_peaks(distance/prominence 無効、prominence は全 NaN→値降順ソート)、flattop 窓は hanning 代替、savgol は移動平均代替、spectrogram は None 返却。
  • fft_spectrum 以降は一様サンプリングを仮定(fs は中央値で推定)。
  • 多くの統計は nan* 系で NaN を無視。エッジ/パルス/サイクル系は有限値数や Top>Base の前提を満たさないと空 dict / None を返す。

ファイルパス: C:\Users\motto\OneDrive\デスクトップ\グラフ\analysis.py


担当領域: math_adv(波形演算 / 高度解析)

対象ファイル:

  • C:\Users\motto\OneDrive\デスクトップ\グラフ\mathchan.py(数学チャンネル=波形演算)
  • C:\Users\motto\OneDrive\デスクトップ\グラフ\advanced.py(マスク試験/アイ/ジッタ/シリアル解読)

依存:

  • 両モジュールとも import numpy as np
  • advanced.pyimport analysis し、analysis.histogram_top_base(y)auto_threshold で利用。
  • mathchan.unary の積分のみ任意で scipy.integrate.cumulative_trapezoid を使用(無ければ自前フォールバック)。

1. mathchan.py(数学チャンネル)

モジュール docstring 概要: 2系列の四則演算(A±B / A×B / A÷B)と、単一系列の積分・微分・絶対値・二乗・移動平均・ローパス。X(時間軸)が異なる場合は B を A の時間軸へ補間して揃える。

1.1 定数(演算名リスト)

  • BINARY_OPS = ["A+B", "A-B", "A×B", "A÷B"]
    • 注意: 乗算記号は全角「×」、除算は全角「÷」。
  • UNARY_OPS = ["積分 ∫A dt", "微分 dA/dt", "絶対値 |A|", "二乗 A²", "移動平均", "ローパス(RC)"]
    • 文字列はこの正確なラベルで一致判定される(分岐は完全一致)。∫A dtdA/dt|A|(上付き2)に注意。

1.2 内部ヘルパー _sorted_xy(x, y)

  • x, yfloat の ndarray に変換し、np.argsort(x) で x 昇順に並べ替えた (x[order], y[order]) を返す。

1.3 binary(xa, ya, xb, yb, op) — 2系列四則演算

  • 引数: A系列の xa, ya、B系列の xb, yb、演算名 op
  • 処理:
    1. xa, ya を float ndarray 化。
    2. B を _sorted_xy(xb, yb) で x 昇順ソート。
    3. yb_i = np.interp(xa, xbs, ybs)B を A の時間軸 xa に線形補間(A の各点での B の値)。np.interp の既定挙動上、xa が xb 範囲外なら端点値でクランプされる。
    4. np.errstate(divide="ignore", invalid="ignore") の中で演算:
      • "A+B"ya + yb_i
      • "A-B"ya - yb_i
      • "A×B"ya * yb_i
      • "A÷B"np.where(yb_i != 0, ya / yb_i, np.nan)(B=0 の点は NaN)
      • それ以外 → raise ValueError(f"未知の演算: {op}")
  • 戻り値: (xa, r)(X は A の時間軸そのまま、Y は演算結果配列)。

1.4 unary(x, y, op, param=None) — 単一系列演算

  • 引数: x, y(float ndarray 化される)、演算名 opparam(移動平均の窓長 or ローパスのカットオフ[Hz]。既定 None)。
  • 分岐(完全一致):
    • "積分 ∫A dt": まず scipy.integrate.cumulative_trapezoid(y, x, initial=0.0) を試行。例外時フォールバック → dx = np.diff(x, prepend=x[0]); r = np.cumsum(y * dx)。戻り (x, r)
    • "微分 dA/dt": np.gradient(y, x)。戻り (x, np.gradient(y, x))
    • "絶対値 |A|": np.abs(y)。戻り (x, np.abs(y))
    • "二乗 A²": y * y。戻り (x, y*y)
    • "移動平均": win = int(param or 5)既定窓長 5); win = max(1, win); kernel = np.ones(win)/win; r = np.convolve(y, kernel, mode="same")。戻り (x, r)
    • "ローパス(RC)": 1次 RC ローパスを前進差分で適用。
      • fc = float(param or 1000.0)既定カットオフ 1000.0 Hz
      • dt = np.median(np.diff(x)) if x.size > 1 else 1.0
      • if dt <= 0 or fc <= 0: return x, y.copy()(ガード: 変化なしで返す)
      • alpha = dt / (dt + 1.0 / (2*np.pi*fc))
      • 逐次: acc = y[0] から acc += alpha*(y[i]-acc); r[i]=acc(Python ループで全点)。戻り (x, r)
    • いずれにも一致しない → raise ValueError(f"未知の演算: {op}")

2. advanced.py(高度解析)

モジュール docstring 概要: マスク/リミット合否、アイダイアグラム、ジッタ、シリアルプロトコル解読。プロトコルは UART(1線)/ I2C(SCL/SDA)/ SPI(SCK/MOSI[/CS])対応。

2.1 auto_threshold(y) — 自動しきい値

  • top, base = analysis.histogram_top_base(y)
  • top is None のとき → float(np.nanmean(y))(ヒストグラムで上下が取れない場合は平均)。
  • それ以外 → (top + base) / 2.0(HighレベルとLowレベルの中点)。

2.2 _logic(y, threshold) — ロジック化(内部)

  • (np.asarray(y, float) > threshold).astype(np.int8) で 0/1 列(しきい値超過で 1、等値は 0)。

2.3 crossings(t, y, level, edge="both") — レベル交差時刻

  • 引数: t, y(float化)、交差レベル leveledge"rising" / "falling" / "both"、既定 "both")。
  • ロジック: below = y < level
    • 立上り up = np.where(below[:-1] & ~below[1:])[0]
    • 立下り dn = np.where(~below[:-1] & below[1:])[0]
  • 各交差は線形補間: frac = (level - y1)/(y2 - y1)y2==y1 のとき 0.0), 時刻 t[i] + frac*(t[i+1]-t[i])
  • 戻り値:
    • edge=="rising"up の補間時刻配列
    • edge=="falling"dn の補間時刻配列
    • それ以外("both")→ 両者を結合し np.sort した昇順配列。

2.4 _level_at(t, logic, time) — 指定時刻のロジック値(内部)

  • i = int(np.searchsorted(t, time)); i = min(max(i, 0), len(logic)-1); return int(logic[i])

2.5 mask_test(t, y, upper=None, lower=None) — マスク/リミット合否

  • 引数: t, y、上限 upper(既定 None=無効)、下限 lower(既定 None=無効)。
  • viol ブール配列。upper is not Noneviol |= y > upperlower is not Noneviol |= y < lower
  • n = int(viol.sum())
  • 戻り値 dict:
    • "passed": n == 0(bool)
    • "violations": n(違反サンプル数, int)
    • "mask": 違反位置のブール配列
    • "violation_times": t[viol](違反した時刻の配列)

2.6 eye_diagram(t, y, symbol_period, n_ui=2) — アイダイアグラム

  • 引数: t, y、シンボル周期 symbol_periodn_ui(折返しUI数, 既定 2)。
  • symbol_period <= 0(None, None)
  • span = n_ui * symbol_period; phase = (t - t[0]) % span
  • 戻り値: (phase, y)(phase に対して y を重ね描きするとアイ波形になる)。

2.7 jitter_tie(t, y, threshold=None, edge="rising") — ジッタ(TIE)

  • 引数: t, ythreshold(None なら auto_threshold(y))、edge(既定 "rising")。
  • cr = crossings(t, y, threshold, edge)len(cr) < 3{}(空 dict)。
  • 理想クロックを最小二乗直線でフィット: a, b = np.polyfit(idx, cr, 1); ideal = a*idx + b; tie = cr - ideal
  • 戻り値 dict:
    • "tie": TIE 配列
    • "crossings": 交差時刻 cr
    • "rms": float(np.std(tie))
    • "pp": float(tie.max() - tie.min())
    • "period": float(a)(推定周期=直線傾き)
    • "freq": float(1.0/a) if a else None
    • "edges": int(len(cr))

2.8 decode_uart(t, y, baud, threshold=None, bits=8, parity="none", stop_bits=1, idle="high", lsb_first=True) — UART解読(1線)

  • 必須引数: baud(ボーレート)。任意: threshold(None→auto_threshold)、bits(既定8)、parity"none"/"even"/"odd"、既定"none")、stop_bits(既定1)、idle"high"/"low"、既定"high")、lsb_first(既定True)。
  • 前処理: logic = _logic(y, threshold)idle=="low" なら logic = 1 - logic(反転論理)。bit_t = 1.0/float(baud)
  • 検出ループ: logic[i]==1 and logic[i+1]==0(立下り)をスタートビット候補。
    • スタートビット中央 start_t + 0.5*bit_t で 0 を確認(違えばスキップ)。
    • データビット: k=0..bits-1bt = start_t + (1.5 + k)*bit_t_level_at でサンプル。bt > tend なら ok=False で中断。
    • ビット組立て: pos = k if lsb_first else (bits-1-k); val |= (bitval&1) << pos
    • パリティ(parity in ("even","odd")): start_t + (1.5 + bits)*bit_t でパリティビット取得。ones = bin(val).count("1") + pb。even で奇数/odd で偶数なら ok=Falseppos を 1 進める。
    • ストップビット: start_t + (1.5 + ppos)*bit_t が 1 でなければ ok=False
    • ch = chr(val) if 32 <= val < 127 else ""(印字可能 ASCII のみ文字化)。
    • 次フレームへ前進: adv_t = start_t + (0.5 + ppos + stop_bits)*bit_t; j = searchsorted(t, adv_t); i = max(j, i+1)
  • 戻り値: list of dict。各要素キー:
    • "time": スタートビット先頭時刻 float(start_t)
    • "value": int(val)
    • "hex": f"0x{val:02X}"(2桁大文字16進)
    • "char": 印字可能文字 or ""
    • "ok": bool(ok)(パリティ/ストップ/データ完結の総合判定)

2.9 decode_i2c(t, scl, sda, threshold=None) — I2C解読

  • 引数: tsclsdathreshold(None なら auto_threshold(np.concatenate([scl, sda])) =SCL/SDA結合で算出)。
  • ロジック化 Lscl, Lsda
  • エッジ抽出:
    • scl_rise: SCL 立上り index+1(ビットサンプル点)
    • sda_fall: SDA 立下り(START候補)
    • sda_rise: SDA 立上り(STOP候補)
  • scl_high(i): その index で SCL=1 か。
  • START = SCL High 中の SDA 立下り;STOP = SCL High 中の SDA 立上り。markers を index 昇順で (idx,"S"/"P")
  • ビット収集: SCL 立上りごとに _level_at(t, Lsda, t[i])bits に追加。9ビット溜まると flush_byte()
  • flush_byte(): 9ビット以上のとき上位8ビットを data、9ビット目を ackval を MSB先頭で組立て。
    • 最初のバイト(first_byte)→ type="addr": addr = val>>1, rw = "R" if (val&1) else "W"
    • 以降 → type="data"
    • ACK: "ACK" if ack==0 else "NACK"(0=ACK)。
  • 戻り値: list of dict。種別ごとのキー:
    • START: {"time", "type": "START"}
    • STOP: {"time", "type": "STOP"}
    • アドレス: {"time", "type": "addr", "value": addr, "hex": f"0x{addr:02X}", "rw": "R"/"W", "ack": "ACK"/"NACK"}
    • データ: {"time", "type": "data", "value": val, "hex": f"0x{val:02X}", "ack": "ACK"/"NACK"}
    • time は該当 index の float(t[...])

2.10 decode_spi(t, sck, mosi, cs=None, threshold=None, cpol=0, cpha=0, bits=8, msb_first=True) — SPI解読

  • 引数: tsckmosics(任意, 既定None)、threshold(None→auto_threshold(np.concatenate([sck, mosi])))、cpol(既定0)、cpha(既定0)、bits(既定8)、msb_first(既定True)。
  • ロジック化: Lsck, LmosiLcs = _logic(cs, th) if cs is not None else None
  • サンプルエッジ決定: rising/falling を求め、sample_edges = rising if (cpol==cpha) else fallingnp.sort
  • 各サンプルエッジで:
    • Lcs があり該当 index で Lcs==1CS=High=非選択)→ ビットをリセットしスキップ(cur,nbits,start_idx = 0,0,None)。
    • start_idx 未設定なら設定。bit = _level_at(t, Lmosi, t[i])
    • msb_firstcur = (cur<<1)|(bit&1)、else → cur |= (bit&1)<<nbits
    • nbits==bits で1バイト確定。
  • 戻り値: list of dict。各要素キー:
    • "time": float(t[start_idx])(バイト先頭ビットの時刻)
    • "value": cur(int)
    • "hex": f"0x{cur:0{(bits+3)//4}X}"(ビット数に応じた桁数の大文字16進。例 bits=8→2桁、bits=16→4桁)
    • 注意: UART/I2C と異なり SPI の戻りには ok/ack/char キーは無い。

3. 再現上の注意・既知の制約

  • 演算名/プロトコルパラメータの文字列・既定値は上記の通り厳密一致が必要(特に全角記号 ×・÷・∫・²・|A|・dA/dt)。
  • _logic のしきい値判定は「超過(>)で1」。等値は0。auto_threshold が None を返す経路(ヒストグラム失敗)では平均値が使われる。
  • decode_uart のサンプル位置はスタート立下り基準で (1.5 + k)*bit_t(ビット中央)。stop_bits は前進量計算のみに使用(複数ストップビットの個別検証はしない)。
  • decode_i2c のACK極性は 0=ACK / 1=NACK。バイトは常に MSB先頭で組立て、最初のバイトをアドレス(7bit+R/W)と解釈。
  • decode_spi の CS は Active-Low 前提(CS=1 を非選択として無視)。サンプルエッジは cpol==cpha のとき立上り、そうでなければ立下り。
  • jitter_tie は交差が3未満で空 dict。a==0(傾き0)のとき freq は None。
  • eye_diagramsymbol_period<=0(None, None)
  • binaryA÷B は B=0 点で NaN を返す(errstate で警告抑制)。np.interp の範囲外は端点クランプ。

gui_core 仕様(graph_app.py / アプリ全体構造・画面レイアウト)

0. ファイル冒頭・モジュール構成

  • ファイル冒頭は docstring("""CSV / TSV / 波形データ グラフ・解析ツール(PySide6 / Qt)。""")。使い方は python graph_app.py。GUI は matplotlib の qt_compat 経由で実装され PySide6 / PyQt6(Qt6系)で動作する旨を明記。
  • import:
    • import os, import sys
    • from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets(Qt バインディングは qt_compat 経由で取得。直接 PySide6 を import しない)
    • from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar
    • from matplotlib.figure import Figure
    • 自作モジュール: advanced, analysis, config_io, data_loader, jp_font, mathchan, plotter
  • モジュール定数:
    • PREVIEW_ROWS = 100
    • UserRole = QtCore.Qt.ItemDataRole.UserRole
    • DECIMATE_TARGET = 8000(折れ線/散布図でこの点数超で間引き)
    • BUSY_ROWS = 200_000(この行数超で待機カーソル)
  • 補助関数 _parse_float(text, default=None): 空文字なら default、float() 失敗(ValueError)でも default を返す。
  • class CheckListWidget(QtWidgets.QListWidget): mousePressEvent をオーバーライドし、行のどこをクリックしてもチェックがトグル。item.flags() & QtCore.Qt.ItemFlag.ItemIsUserCheckable が立っている項目で checkState()==QtCore.Qt.CheckState.Checked を判定し Unchecked/Checked を反転、event.accept()。それ以外は super().mousePressEvent

Qt6 スコープ列挙の徹底: 全列挙値を完全修飾で書く。例: QtCore.Qt.ItemDataRole.UserRole, QtCore.Qt.ItemFlag.ItemIsUserCheckable, QtCore.Qt.CheckState.Checked/Unchecked, QtCore.Qt.Orientation.Horizontal/Vertical, QtCore.Qt.TextElideMode.ElideNone, QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel, QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded/ScrollBarAlwaysOff, QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection/NoSelection, QtWidgets.QAbstractItemView.EditTrigger.*, QtCore.Qt.CursorShape.WaitCursor, QtWidgets.QFrame.Shape.HLine/NoFrame, QtWidgets.QFrame.Shadow.Sunken, QtWidgets.QMessageBox.StandardButton.Yes

1. class GraphApp(QtWidgets.QMainWindow) / init

  • 状態属性(初期値):
    • self.datasets = {} # label -> DataFrame
    • self.meta = {} # label -> {"path","enc","delim"}
    • self.series_styles = {} # "file\tcol" キー -> style dict
    • self.last_dir = os.path.expanduser("~")
    • self._suspend_redraw = True(構築・設定適用中の自動再描画抑制。__init__ 末尾で False
    • self._has_drawn = False(一度でも描画したか)
    • self.font_name = jp_font.setup_japanese_font()
    • ウィンドウタイトル "CSV / TSV / 波形 グラフ・解析ツール"self.resize(1280, 800)self.setAcceptDrops(True)
    • self._redraw_timer(QTimer, SingleShot, timeout→_do_live_redraw
    • ズーム再サンプル状態: self._dyn=[], self._dyn_cid=None, self._resampling=False, self._resample_timer(SingleShot, timeout→_do_resample
    • self.recent_files = [](MRU)
    • カーソル測定状態: self._cursor_cid=None, self._cursor_pts=[], self._cursor_artists=[], self._cursors=[], self._cursor_drag=None, self._cursor_text=None
  • 構築順: _build_menu()_build_central()_build_statusbar()_on_chart_type_change()_try_restore_session()。復元失敗時にステータスへ案内文「『データ』タブでファイル追加、または「ファイル」→「サンプルを開く」でお試しください。」を表示。最後に self._suspend_redraw = False

2. _build_menu とメニュー/ショートカット

  • ヘルパ _menu_action(menu, label, slot, shortcut=None, tip=None): QtGui.QAction(label, self) を生成、shortcut/tip 設定、triggered.connect(slot)menu.addAction(act)、act を返す。
  • メニューバー構成(self.menuBar()):
    • ファイル(&F):
      • 「ファイル追加...」→ add_file, Ctrl+O
      • 「サンプルを開く」→ open_sample, Ctrl+E, tip「同梱の波形サンプルを開いてすぐ試せます」
      • サブメニュー「最近使ったファイル」(self.recent_menu_rebuild_recent_menu() で構築)
      • (区切り)
      • 「グラフ画像を保存...」→ save_figure, Ctrl+S
      • 「ファイルごとに一括画像出力...」→ batch_export, Ctrl+B
      • 「クリップボードにコピー」→ copy_figure, Ctrl+Shift+C
      • (区切り)
      • 「設定を保存...」→ save_config_dialog, Ctrl+Shift+S
      • 「設定を読み込み...」→ load_config_dialog, ショートカットなし
      • (区切り)
      • 「終了」→ self.close, Ctrl+Q
    • 表示(&V):
      • 「グラフを描画」→ draw_graph, F5
      • 「全データに合わせる(オートスケール)」→ auto_scale_scope, なし
    • 解析(&A):
      • 「解析実行(ピーク・測定)」→ run_analysis, Ctrl+R, tip「選択中の解析対象系列のピーク・測定値を計算」
      • 「FFTスペクトル表示」→ show_fft, なし
    • ヘルプ(&H):
      • 「使い方」→ show_help, F1
      • 「バージョン情報」→ show_about, なし

3. _build_central(3ペインレイアウト)

  • 中央ウィジェット内に QHBoxLayout(contentsMargins 6,6,6,6)→ 横方向 QSplitter
  • 左ペイン: QTabWidgetself.tabssetMinimumWidth(220))。タブ3つ:
    • _build_tab_data() → ラベル「1. データ」
    • _build_tab_scope() → ラベル「2. オシロ/解析」
    • _build_tab_advanced() → ラベル「3. 高度解析」
  • 中央ペイン: 縦 QSplitter。上=_build_plot_area()(グラフ表示)、下=_build_preview()(データ編集表)。setStretchFactor(0,4), setStretchFactor(1,1)
  • 右ペイン: _build_format_panel()(グラフ書式調整パネル)。
  • 横スプリッタ: setStretchFactor(0,0)(左固定気味)/(1,1)(中央伸びる)/(2,0)(右固定気味)。初期サイズ splitter.setSizes([320, 720, 400])
  • 構築後 self._wire_live_signals()self._add_tooltips() を呼ぶ。

4. _build_tab_data(データタブ)

  • 全体: QWidgetQVBoxLayout(margins 0)→ 縦 QSplittervsplit)。上段=ファイル一覧、下段=読込設定/XY/描画。vsplit.setStretchFactor(0,0)/(1,1)vsplit.setSizes([140, 520])
  • 上段(top) QVBoxLayout(margins 2):
    • 太字ラベル _bold("読み込み済みファイル")
    • ヒント QLabel("ファイルを追加(ここにドラッグ&ドロップも可)→ X/Y を選び「グラフを描画」")、WordWrap、color:#666;
    • self.file_list = QtWidgets.QListWidget():
      • setMinimumHeight(60)
      • ツールチップ「読み込んだファイル一覧。選択するとプレビューを表示します。\n長い名前は横スクロール/ホバーで全体を表示。\n縦幅は下の境界線、横幅は左パネルとグラフの境界線をドラッグで変えられます。」
      • setTextElideMode(ElideNone), setWordWrap(False)(長名を省略せず表示)
      • setHorizontalScrollMode(ScrollPerPixel), setHorizontalScrollBarPolicy(ScrollBarAsNeeded)(横スクロールで全体表示)
      • setSelectionMode(ExtendedSelection)(Ctrl/Shift+クリックで複数選択)
      • currentRowChanged.connect(self._on_file_selected)
    • ボタン行(QHBoxLayout): 「ファイル追加...」→add_file、「サンプルを開く」(tip「同梱の波形サンプルを開いてすぐ試せます」)→open_sample、「削除」(tip「選択中のファイルを削除(Ctrl/Shift+クリックで複数選択→まとめて削除)」)→remove_file、「全削除」(tip「読み込み済みファイルをすべて一覧から削除します。」)→clear_all_files
  • 下段(bottom) QVBoxLayout(margins 2):
    • 読込設定 QGridLayout:
      • QLabel("区切り:") + self.delim_combo(先頭 "自動判別"、続けて data_loader.DELIMITER_LABELS.values() を追加。tip「区切り文字。変更後は「選択中ファイルを再読込」を押してください。」)
      • QLabel("文字コード:") + self.enc_combo(項目 ["自動判別","utf-8-sig","utf-8","cp932","shift_jis","euc-jp","utf-16"]、tip「文字化けする場合はここで指定し、「選択中ファイルを再読込」を押します。」)
      • ボタン「選択中ファイルを再読込」(tip「区切り・文字コードの変更を反映して読み直します。」)→reload_current(grid に colspan 2 で配置)
    • _hline()(HLine + Sunken の区切り線)
    • QLabel("X軸(横軸 / ラベル)")
    • self.xleft_check = QtWidgets.QCheckBox("一番左の列をX軸にする(位置で固定)")、tip「ONにすると各ファイルの『一番左の列』をX軸に使います(列名が違っても適用)。\n複数ファイル/バッチ出力でX軸を固定したいときに便利。\nOFFなら下のコンボで列名を指定します。」、toggled.connect(self._on_xleft_toggled)
    • self.x_combo = QtWidgets.QComboBox()、tip「横軸に使う列(列名で指定)。波形なら時間列を選びます。」、currentTextChanged.connect(self._on_x_changed)
    • QLabel("Y軸(値)※チェックした系列を描画(行のどこでもクリックでON/OFF)")
    • self.y_list = CheckListWidget(): setSelectionMode(NoSelection)、tip「描画したい系列にチェック。行のどこをクリックしてもON/OFFできます。」、setStyleSheet("QListWidget::indicator { width:16px; height:16px; }")itemChanged.connect(self._on_y_check_changed)itemDoubleClicked.connect(self._on_y_double_clicked)
    • Yボタン行: 「全選択」→_check_all_y(True)、「全解除」→_check_all_y(False)、「反転」→_invert_y
    • self.live_check = QtWidgets.QCheckBox("リアルタイム更新(変更を即反映)")setChecked(True)、tip「オンにすると設定変更が自動で描画に反映されます。大容量データではオフ推奨。」
    • self.decimate_check = QtWidgets.QCheckBox("大容量データを間引き表示(高速・ズームで自動再サンプル)")setChecked(True)、tip「折れ線/散布図で点数が多いとき、見た目を保ったまま間引いて高速描画します。」
    • 描画ボタン行: 「グラフを描画 (F5)」(setStyleSheet("font-weight:bold; padding:6px;")、stretch 2)→draw_graph、「一括画像保存...」(padding:6px;、stretch 1、tip「読み込んだ各ファイルを個別に描画し、ファイル名ごとの画像として一括保存します(タイトル・形式・DPI等は次の画面で調整できます)。」)→batch_export

5. _build_format_panel / _build_style_box(右パネル)

  • _build_format_panel: 縦 QSplittersetMinimumWidth(360))。
    • 上段: QScrollAreasetWidgetResizable(True)setFrameShape(NoFrame))の中に _build_tab_graph()(種別/タイトル/軸/近似曲線/縦横比/画像出力の書式コントロール)
    • 下段: _build_style_box()(系列スタイル表)
    • setStretchFactor(0,3)/(1,2)
  • _build_style_box: QWidget + QVBoxLayout(margins 4)。太字ラベル「系列スタイル(系列名はダブルクリックで変更可)」。
    • self.style_table = QtWidgets.QTableWidget(0, 8)、列見出し ["系列名","色","線種","幅","マーカー","軸","種別","誤差列"]
    • horizontalHeader().setStretchLastSection(True)
    • setEditTriggers(DoubleClicked | EditKeyPressed)
    • itemChanged.connect(self._on_style_label_edited)

6. _build_preview(中央下:データ編集表)

  • QGroupBox("データ編集(選択中ファイル・先頭100行)") + QVBoxLayout(margins 4)。
  • self._preview_label=Noneself._preview_loading=False
  • ツールバー行(QHBoxLayout):
    • self.edit_check = QtWidgets.QCheckBox("編集可")、tip「セルをダブルクリックで編集。値はその場でDataFrameに反映され、グラフにも反映されます。」、toggled.connect(self._on_edit_toggle)
    • ボタン4つ(テキスト, slot, tip): 「行追加」→_row_add「末尾に空行を追加」/「行削除」→_row_del「選択した行を削除」/「列追加」→_col_add「新しい数値列を追加」/「CSV保存」→_save_csv「編集後のデータをCSV/TSVに書き出し」
    • 末尾 addStretch(1)
  • self.table = QtWidgets.QTableWidget(): setEditTriggers(NoEditTriggers)(初期は編集不可)、setAlternatingRowColors(True)itemChanged.connect(self._on_cell_edited)

7. _build_plot_area(中央上:グラフ表示)

  • QWidget + QVBoxLayout(margins 0)。
  • 系列ON/OFFバー self.series_bar = QtWidgets.QScrollArea(): setWidgetResizable(True)setFixedHeight(34)setFrameShape(NoFrame)、横スクロール ScrollBarAsNeeded、縦 ScrollBarAlwaysOff。内側 QWidget+self.series_bar_layout(QHBoxLayout, margins 6,2,6,2)。初期 setVisible(False)。折れ線/散布図のときだけ表示(_rebuild_series_bar)。
  • self.fig = Figure(figsize=(6, 4.4), dpi=100)self.ax = self.fig.add_subplot(111)self.canvas = FigureCanvas(self.fig)self.toolbar = NavigationToolbar(self.canvas, wrap)。レイアウトは toolbar → canvas(stretch 1)。
  • self._plotted_artists=[]。オシロ操作のため canvas に button_press_event/motion_notify_event/button_release_event/scroll_event を mpl_connect。初期は _draw_placeholder()

8. ファイル読込/削除メソッド群

  • add_file: QFileDialog.getOpenFileNames(self, "ファイルを追加(複数選択可)", self.last_dir, "データ (*.csv *.tsv *.txt);;すべて (*.*)") → 各 _load_file → last_dir 更新 → _refresh_columns
  • _load_file(path):
    • enc: enc_combo 値が "自動"始まりなら None。
    • delim: delim_combo 値が "自動"始まりでなければ DELIMITER_LABELS を逆引きして区切り文字を得る。
    • size > 5_000_000(5MB)超で setOverrideCursor(WaitCursor) + ステータス「読み込み中… {basename}」+ processEvents()
    • df, used_enc, used_delim = data_loader.load_table(path, encoding=enc, delimiter=delim)。例外時 QMessageBox.critical(self, "読み込みエラー", ...) で return。finally で restoreOverrideCursor()
    • label = basename。同名衝突かつ別パスなら "{base} ({i})"(i=2 から)で一意化。
    • self.datasets[label]=dfself.meta[label]={"path","enc":used_enc,"delim":used_delim}、一覧未登録なら _add_file_item(label)_push_recent(path)、ステータス「{label} を読み込み({行}行 × {列}列, {used_enc})」。
  • _add_file_item(label): QListWidgetItem(label)setToolTip(label)(ホバーで全名表示)して追加。
  • MRU: _push_recent は重複除去して先頭挿入、del self.recent_files[12:](最大12件)、_rebuild_recent_menu。空時は「(履歴なし)」を無効項目で表示。_open_recent はファイル無ければ情報ダイアログ + 履歴から除去。
  • remove_file: selectedItems()(無ければ currentItem)→ labels。空なら情報ダイアログ。2件以上は QMessageBox.question(self, "一括削除", "{n} 個のファイルを一覧から削除しますか?")、Yes 以外は中止。_remove_labels(labels) 後ステータス「{n} 個のファイルを削除しました。」
  • clear_all_files: datasets 空なら情報。question(self, "全削除", "読み込み済みの {n} 個すべてを一覧から削除しますか?") Yes で _remove_labels(全keys)、ステータス「すべて({n} 個)のファイルを削除しました。」
  • _remove_labels(labels): datasets/meta から pop。series_styles から key.split("\t",1)[0] が対象ラベルのものを掃除。file_list を blockSignals して該当項目を逆順 takeItem。_refresh_columns()。残あれば setCurrentRow(0)、無ければ preview/table クリア + _draw_placeholder()
  • reload_current: currentItem 無ければ情報。meta[text]["path"] を取得し datasets/meta/list から除去 → _load_file(path)_refresh_columns
  • _on_file_selected(_row): currentItem が datasets にあれば _populate_preview(df, label)enc_combo を meta["enc"] に合わせる(findText>=0 のときのみ)。

9. X軸(左端固定)ロジック

  • _on_xleft_toggled(on): _refresh_columns()(Y候補再構築)→ _update_x_combo_enabled()
  • _update_x_combo_enabled(): plotter.CHART_INFO[現在の種別].get("use_x", True) かつ not _use_leftmost_x() のとき x_combo.setEnabled(True)
  • _use_leftmost_x(): xleft_check が存在し isChecked() なら True。
  • _x_values(df): 左端ON→df.iloc[:,0].to_numpy()。OFF→x_combo.currentText() が列にあればその列、無ければ先頭列。
  • _effective_x_label(): 左端ONなら選択系列の先頭ファイル先頭列名、OFFなら x_combo.currentText()
  • _refresh_columns(): X候補=全DataFrame列名の和集合(出現順)。Y候補=各ファイルの各列を (label, col) を UserRole に保持して列挙。複数ファイル時の表示名は "{label} | {col}"、単一なら c左端X ON時は各ファイルの ci==0(先頭列)をY候補から除外。以前のチェック状態は (label, col) 識別子で復元。フラグは ItemIsUserCheckable | ItemIsEnabled。末尾で _on_y_selection_changed()

10. Y軸チェック操作

  • _on_y_check_changed: suspend中は無視、それ以外 _on_y_selection_changed()
  • _set_all_checks(func): blockSignals して各行に func(item) の真偽で Checked/Unchecked、最後 _on_y_selection_changed()
  • _check_all_y(checked)=全行同値、_invert_y=現状反転。
  • _on_y_double_clicked(item): その行だけ ON にして draw_graph()
  • _selected_series_items(): チェック済み行の (file_label, col, display_text) を順序保持で返す。

11. 系列スタイル表ロジック(_rebuild_style_table 等)

  • キー: _style_key(fl, col) = f"{fl}\t{col}"(表示名変化に不依存)。
  • series_styles は欠損時 dict(plotter.DEFAULT_STYLE)setdefaultDEFAULT_STYLE={"color":None,"linestyle":"-","linewidth":1.5,"marker":"","markersize":4.0,"alpha":1.0}
  • _rebuild_style_table(): _suspend_redraw=True で構築、縦スクロール位置を保存/復元、行数=選択系列数。各行8列:
    • 列0 系列名: st.get("label") or disp、UserRole にキー、ItemIsEditable 付与。
    • 列1 色: QPushButton(st.get("color") or "自動")、色ありなら背景色 stylesheet、クリック→_pick_color(key, btn)
    • 列2 線種: QComboBoxplotter.LINESTYLES のキー)。LINESTYLES={"実線":"-","破線":"--","一点鎖線":"-.","点線":":","なし":"None"}。変更→_set_style(key,"linestyle",値)
    • 列3 幅: QDoubleSpinBox range(0.2,10) step 0.5、初期=st["linewidth"]。→_set_style(...,"linewidth",v)
    • 列4 マーカー: QComboBoxplotter.MARKERS キー)。MARKERS={"なし":"","丸":"o","四角":"s","三角":"^","菱形":"D","× ":"x","+":"+","点":"."}
    • 列5 軸: QComboBoxplotter.SERIES_AXES)。SERIES_AXES={"主軸":"primary","第2軸":"secondary"}、既定 "主軸"。
    • 列6 種別: QComboBoxplotter.SERIES_KINDS)。SERIES_KINDS={"自動":"","折れ線":"line","棒":"bar","面":"area","散布図":"scatter"}、既定 "自動"。
    • 列7 誤差列: QComboBox、先頭 "なし" + 同ファイルの全列名。→_set_style(...,"errcol", None if "なし" else v)
  • _pick_color(skey, btn): QColorDialog.getColor → 有効なら hex を _set_style(...,"color",hex)、ボタンのテキスト/背景更新。
  • _set_style(skey, attr, value): series_styles を setdefault しつつ [attr]=value_request_redraw()
  • _on_style_label_edited(item): 列0 のみ。UserRole のキーで st["label"]=text or None_request_redraw()

12. データ編集メソッド群

  • _populate_preview(df, label=None): _preview_loading=True の間に df.head(PREVIEW_ROWS=100) を表に流し込む。NaN は空文字、それ以外 str(v)resizeColumnsToContents()。label 指定で self._preview_label 更新。
  • _on_edit_toggle(on): ON で DoubleClicked|EditKeyPressed|AnyKeyPressed、OFF で NoEditTriggers
  • _on_cell_edited(item): 構築中(_preview_loading)/未選択は無視。対象 DataFrame の iat[r,c] を更新。数値列(pd.api.types.is_numeric_dtype)は pd.to_numeric(text, errors="coerce")(不可は NaN)、非数値は文字列。ステータス「編集: {label} 行{r}「{col}」= {text}」、_request_redraw()
  • _edit_target(): _preview_label が datasets に無ければ情報「左の一覧でファイルを選択してください。」を出し None。
  • _row_add: 末尾に [np.nan]*列数df.loc[len(df)] で追加、reset_index、再描画。len(df)>100 ならステータスに全行数/先頭100行注記。
  • _row_del: table.selectedIndexes() の行集合を降順で df.drop → reset_index → 再描画。未選択時ステータス「削除する行を選択してください。」
  • _col_add: QInputDialog.getText(self, "列追加", "新しい列名:")。空/キャンセルは無視。同名は QMessageBox.warning(self, "列追加", "同名の列が既にあります。")df[name]=0.0 で初期化、preview 再描画、_refresh_columns()、ステータス「列「{name}」を追加(値0.0で初期化)」。
  • _save_csv: 既定名は .csv/.tsv 末尾でなければ +".csv"QFileDialog.getSaveFileName(self, "CSV/TSVとして保存", default, "CSV (*.csv);;TSV (*.tsv);;全てのファイル (*.*)")。拡張子 .tsv なら sep="\t" それ以外 ","、to_csv(path, index=False, sep=sep, encoding="utf-8-sig")。成否ステータス/エラーダイアログ。

13. ライブ更新まわり

  • _wire_live_signals(): chart_combo / title・xlabel・ylabel・各 SpinBox / grid・legend・pct・xlog・ylog / legend_loc / trend系 / aspect_w,h / xmin,xmax,ymin,ymax(editingFinished) / オシロ各コントロール(scope_check, tdiv, vdiv, xpos, ypos, xdivs, ydivs, show_peaks_check, npeaks, smooth_spin)を _request_redraw へ接続。
  • _request_redraw(): _suspend_redraw または未描画なら何もしない。live_check 未チェックなら何もしない。_redraw_timer.start(180)(180ms デバウンス)。
  • _do_live_redraw(): datasets あり かつ _has_drawndraw_graph()

14. その他(gui_core 関連)

  • _build_statusbar(): ステータスバーに常設ラベルで「日本語: {font_name}」または「日本語フォント未検出」。
  • _bold(text)/_hline() は静的ヘルパ。_set_status(text)=status.showMessage
  • D&D: dragEnterEvent は URL あれば accept。dropEvent.csv/.tsv/.txt のみ受理し各 _load_file_refresh_columns、描画済みなら draw_graph
  • open_sample: スクリプトと同階層 サンプルデータ/ 配下の 波形_減衰振動.csv月別売上.csv を順に試行。読み込めたら X=先頭列、Y候補は data_loader.numeric_columns の先頭(無ければ2列目/先頭)にチェック、種別「折れ線」で draw_graph。見つからなければ情報ダイアログ。
  • show_help/show_about: それぞれ QMessageBox.information/QMessageBox.about で使い方・バージョン情報(PySide6 + matplotlib、日本語フォント名)を表示。

15. エントリポイント

  • main(): QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)GraphApp()win.show()sys.exit(app.exec())if __name__ == "__main__": main()

関連ファイル(絶対パス)

  • C:\Users\motto\OneDrive\デスクトップ\グラフ\graph_app.py
  • C:\Users\motto\OneDrive\デスクトップ\グラフ\plotter.py(CHART_TYPES/CHART_INFO/DEFAULT_STYLE/LINESTYLES/MARKERS/LEGEND_LOCS/TRENDLINES/SERIES_KINDS/SERIES_AXES)
  • C:\Users\motto\OneDrive\デスクトップ\グラフ\data_loader.py(DELIMITER_LABELS, load_table, numeric_columns)

gui_features(描画フローと機能挙動)— 詳細仕様

対象ファイル: C:\Users\motto\OneDrive\デスクトップ\グラフ\graph_app.py(クラス GraphApp(QtWidgets.QMainWindow))。GUI は matplotlib の matplotlib.backends.qt_compat(PySide6/PyQt6)と backend_qtaggFigureCanvasQTAgg/NavigationToolbar2QT を使用。外部モジュール plotter/analysis/advanced/mathchan/config_io/data_loader/jp_font を参照。

モジュール定数(graph_app.py 冒頭)

  • PREVIEW_ROWS = 100(プレビュー表示行数 df.head(100))。
  • UserRole = QtCore.Qt.ItemDataRole.UserRole
  • DECIMATE_TARGET = 8000(折れ線/散布図でこの点数超で間引き)。
  • BUSY_ROWS = 200_000(描画点数がこれ超で待機カーソル)。
  • _parse_float(text, default=None): 空欄/変換失敗で default

系列スタイルの保持キー

  • _style_key(fl, col)f"{fl}\t{col}"(ファイル名 TAB 列名)。表示名変化に依存しない安定キー。series_styles 辞書に保持。各値は plotter.DEFAULT_STYLE のコピー+上書き。DEFAULT_STYLE = {"color": None, "linestyle": "-", "linewidth": 1.5, "marker": "", "markersize": 4.0, "alpha": 1.0}label/axis/kind/errcol も追加保持されうる。

draw_graph(描画フロー)

  1. self.datasets が空 → 情報ダイアログ「先にファイルを追加してください。」で return。
  2. Y系列未選択(_selected_series_items() 空)はポップアップを出さず_draw_placeholder()_rebuild_series_bar(現在の種別) → ステータス「Y軸(値)の系列をチェックするとグラフを表示します。」で return。
  3. ctype = self.chart_combo.currentText()issues=[] 警告蓄積リスト。
  4. xlim=_range_pair(xmin,xmax,"X",issues), ylim=_range_pair(ymin,ymax,"Y",issues)_range_pair は両端を _field_float(空=(None,True)、変換失敗=(None,False))で読み、読めない時「X軸 最小値を数値として読めません」等を追加。min≥max のとき「X軸 最小≥最大のため範囲指定を無視しました」を追加し (None,None) を返す。
  5. scope=self._scope_dict()scope["enabled"] かつ ctype in ("折れ線","散布図")t_per_div/v_per_div のどちらかが正でなければ「time/div・V/div は正の値が必要(オシロ表示を無効化)」を追加し enabled=False に。
  6. _clear_dynamic_resample(), _reset_figure_axes()(メイン軸 self.ax 以外=カラーバー軸等を remove()), カーソル状態(_cursor_pts/_cursor_artists/_cursors/_cursor_drag/_cursor_text)をリセット。
  7. series, categories = _build_series(ctype)
  8. Y対数ON かつ Y系列に0以下があれば「Y対数: 0以下の値は表示されません」を追加。X対数ON かつ折れ線/散布図でX系列に0以下があれば「X対数: 0以下の値は表示されません」を追加(_has_nonpositive:pandas で数値化→有限値の最小が ≤0 か)。
  9. デシメーション: total = 全系列の len(y) 合計max_points = DECIMATE_TARGET if (decimate_check.isChecked() and ctype in ("折れ線","散布図") and total>DECIMATE_TARGET) else 0
  10. busy = total > BUSY_ROWS のとき待機カーソル+ステータス「描画中…({total:,} 点)」。
  11. markers = _peak_markers() if show_peaks_check.isChecked() else Nonesec_label = 第2軸系列ラベルを " / " 連結
  12. plotter.plot_series(self.ax, series, ctype, ...) を以下引数で呼ぶ: categories, bins=bins_spin.value(), title=title_edit.text(), xlabel=xlabel_edit.text() or _effective_x_label(), ylabel=ylabel_edit.text(), grid, legend, legend_loc, xlim, ylim, xlog, ylog, pct=pct_check, fonts=_fonts(), scope, markers, max_points, trendline={"type":trend_combo,"degree":trend_degree,"window":trend_window,"show_eq":trend_eq}, data_labels=data_labels_check, secondary_label=sec_label
  13. _apply_aspect() →(縦横比固定)→ fig.tight_layout()(例外無視)→ canvas.draw()
  14. max_points ありなら _setup_dynamic_resample(series,ctype,max_points)
  15. _rebuild_series_bar(ctype)_plotted_artists を「ラベルに『近似』を含まない ax.get_lines()」から [(label, line)] で再構築(カーソル追従用)。_has_drawn=True
  16. ステータス「『{ctype}』を描画しました(系列 {n})。」+デシメート時「({total:,}点を間引き表示)」+issues あれば「 ⚠ 」+ " / " 連結。
  17. 例外時は QMessageBox.critical(self,"描画エラー",str(e))

_build_series(chart_type)

  • _selected_series_items() 空で ValueError("Y軸(値)の系列を選択してください。")
  • xname=x_combo.currentText()lbl(fl,col,disp,default) は style の label 優先、無ければ default。
  • 棒/横棒/積み上げ棒/円: src=items[0][0](最初に選んだ系列のファイル)。_use_leftmost_x() でなく xname が src 列に無ければ ValueError("X軸の列『{xname}』が『{src}』にありません。")categories=_x_values(df)。src ファイルの系列のみ {"label","y","style"} で追加。
  • 折れ線/散布図: 各系列 {"label"(default=disp),"x"=_x_values(df),"y","style","axis"(既定primary),"kind"(既定""),"yerr"}yerr は style の errcol が列に存在すれば該当列。
  • それ以外(ヒストグラム/箱ひげ): {"label"(default=disp),"y","style"}

_x_values(df) / X軸関連

  • _use_leftmost_x(): xleft_check(チェックボックス「一番左の列をX軸にする(位置で固定)」)がON。
  • _x_values: leftmost ON → df.iloc[:,0]、OFF → df[xname](無ければ df.iloc[:,0])。
  • _effective_x_label: leftmost ON → 最初の選択系列ファイルの先頭列名、OFF → x_combo.currentText()
  • _on_xleft_toggled: _refresh_columns()(leftmost ON時、各ファイルの先頭列をY候補から除外)→ _update_x_combo_enabled()(X名コンボの有効=CHART_INFO[ctype].use_x and not leftmost)。

系列ON/OFFバー(グラフ上部)

  • _build_plot_area: series_barQScrollAreasetFixedHeight(34)、水平スクロール=AsNeeded/垂直=AlwaysOff、初期非表示)。内部 series_bar_layout=QHBoxLayout(margins 6,2,6,2)。
  • _rebuild_series_bar(chart_type=None): 既存ウィジェット全削除。折れ線/散布図以外は非表示y_list の全項目(チェック有無に関わらず=利用可能な全Y系列)を QCheckBox で並べる。チェック状態は対応 y_list 項目に同期。表示文字は style の label 優先(無ければ項目テキスト)。色は style の color、無ければチェック時#333/非チェック時#888。フォントは checked で bold。_series_bar_building フラグで構築中トグル無視。末尾に addStretch(1)。バー表示ON。
  • _toggle_series_select(item, on): 構築中(_series_bar_building)なら無視。y_list 該当項目の setCheckState を Checked/Unchecked に切替 → itemChanged 経由で _on_y_check_changed_on_y_selection_changed_request_redraw(自動再描画)。

Y選択リスト(左パネル)

  • y_list = CheckListWidget()(行のどこをクリックしてもチェックがトグル;mousePressEvent でトグル)。SelectionMode.NoSelectionitemChanged_on_y_check_changeditemDoubleClicked_on_y_double_clicked(その系列だけにして draw_graph())。
  • ボタン「全選択」「全解除」「反転」(_check_all_y(True/False)/_invert_y)。
  • 各項目は setData(UserRole,(label,col)) で安定識別。表示名は複数ファイル時 "{label} | {col}"、単一時 c
  • _on_y_selection_changed: _rebuild_style_table()_rebuild_series_bar()_update_analysis_targets()_request_redraw()

オシロ/解析タブ(_build_tab_scope

  • チェック「オシロスコープ表示(折れ線/散布図)」= scope_check
  • time/div [s]: tdiv(編集可コンボ)、項目は plotter.eng_125_sequence(1e-9, 1.0, "s")(1-2-5 系列+"s")、既定 "1ms"
  • V/div: vdiv(編集可コンボ)、plotter.eng_125_sequence(1e-3, 100.0, "")、既定 "500m"
  • X位置(中心)=xpos(既定"0"), Y位置(中心)=ypos(既定"0")。
  • X div数=xdivs(SpinBox 範囲2–20、既定10), Y div数=ydivs(2–20、既定8)。
  • ボタン「自動スケール(解析対象に合わせる)」→auto_scale_scope
  • ヒント文(緑11px): 左ドラッグ=位置移動/右ドラッグ=time/V/div/ホイール=time/div・Shift+ホイール=V/div。
  • _scope_dict() 戻りキー: enabled, t_per_div(=parse_eng(tdiv,1e-3)), v_per_div(=parse_eng(vdiv,1.0)), x_pos(=_parse_float(xpos,0.0)), y_pos, x_divs, y_divs
  • 解析対象analysis_target(コンボ、選択Y系列の表示名で同期)。ピーク数 N:=npeaks(1–50、既定5)。平滑化(点):=smooth_spin(0–501、step2、既定0、0=平滑化なし)。チェック「ピークをグラフに表示」show_peaks_check(既定ON)。チェック「表示範囲のみ測定(ズーム/オシロ窓に追従)」window_meas_check
  • ボタン「解析実行」run_analysis/「FFTスペクトル表示」show_fft/「カーソル測定」cursor_btn(checkable、toggle_cursors)。
  • ピーク表peak_table(列「順位/時間/周波数/値」、maxHeight 160)。測定値表meas_table(列「項目/値」)。
  • ボタン「サイクル統計」show_cycle_stats/「トレンド表示」show_trend位相差 対象2:=phase_target2+ボタン「位相差/遅延」show_phase

オシロ ドラッグ操作

  • _scope_active(): scope_checkON ∧ ctype in ("折れ線","散布図")_has_drawn_cursor_cid is None(カーソルモード非競合)∧ toolbar.mode が空(パン/ズーム非選択)。
  • _shift_held(event): Qt の QApplication.keyboardModifiers() の Shift を優先、無ければ event.key に "shift"。
  • _scope_on_press: active ∧ 軸内 ∧ ボタン1or3 ∧ event.x。_scope_drag に button/shift/px/xlim/ylim/tdiv/vdiv/プロット幅w・高h を記録。
  • _scope_on_motion:
    • 左ドラッグ(button1 ∧ not shift)= パン(位置移動)。px 移動量を data 単位に換算して set_xlim/set_ylim を平行移動。オーバーレイ「位置 X中心=… Y中心=…」(format_eng)。
    • 右ドラッグ or Shift = スケール(div)。中心固定で ntdiv=tdiv*2**(dxpx/150.0), nvdiv=vdiv*2**(-dypx/150.0)xlim=xc±xdivs/2*ntdiv, ylim=yc±ydivs/2*nvdiv。オーバーレイ「{ntdiv}s/div {nvdiv}/div」。
    • オーバーレイ_scope_overlayは右下(0.99,0.02)・緑#7CFC00・monospace・黒半透明背景。
  • _scope_on_release: _scope_drag=None、オーバーレイ除去、現 xlim/ylim から xpos/ypos=中心(:.6g)tdiv=format_eng((x1-x0)/xdivs)+"s"vdiv=format_eng((y1-y0)/ydivs)_suspend_redraw 中に書戻し → draw_graph() で正式再構築。
  • _scope_on_scroll: active ∧ 軸内。step=0.8 if up else 1.25(up=ズームイン)。Shift時は vdiv*=step、通常は tdiv*=step(+"s")。_suspend_redraw中に設定→draw_graph()

カーソル測定

  • toggle_cursors(on): ON で button_press/motion/button_release を connect(_cursor_cid にタプル保持)、_cursors=[](最大2本 {x, vline, marker})。ステータス「カーソル: クリックで2本設置 → 線をドラッグで微調整(波形に追従)」。OFF で disconnect+アーティスト削除。
  • _cursor_track_y(x): _plotted_artists[0] の線の x/y を昇順 np.interp(波形追従)。
  • _add_cursor(x): axvline(x, color="#e6194b", lw=0.9, ls="--") + marker "o" color #e6194b ms6
  • _on_cursor_press: 軸内 ∧ xdata。近傍既存カーソル(px距離<8、_cursor_near)があれば掴んでドラッグ(_cursor_drag)。なければ既に2本のとき全消去(3本目でリセット)→ 追加 → _update_cursor_readout
  • _on_cursor_motion: ドラッグ中は該当カーソルの x と vline/marker を更新→ readout。_on_cursor_release: _cursor_drag=None
  • _update_cursor_readout: 2本そろうと dt=x2-x1, dv=y2-y1, freq=1/dt。テキスト「Δt={format_eng(|dt|)} ΔV={format_eng(|dv|)} 1/Δt={format_eng(|freq|)}Hz」を軸上端中央(0.5,0.98)・赤#e6194b・白枠 bbox に表示し、同文をステータスへ。

ピーク/測定

  • _analysis_xy(): analysis_target 表示名に一致する選択系列の (t,y,disp)。X は数値化、NaN率>0.5 でインデックス(np.arange)を時間軸に。window_meas_checkON ∧ _has_drawn のとき現 xlim 内に絞る(マスク該当≥3点のとき適用)。
  • _peak_markers(): 折れ線/散布図のみ。analysis.find_signal_peaks(y,t=t,n=npeaks,smooth=smooth)[{"x":time,"y":value,"text":"第{rank}","color":"#ff3030"}](time が None のものは除外)。
  • run_analysis(): analysis.analyze(t,y,n_peaks=npeaks,smooth=smooth)peak_table に「第{rank}」「{time*1e3:.4g} ms」(None は"-")「{value:.4g}」。meas_table に各 measurement 名と「{value:.6g} {unit}」(None は"-")。show_peaks_checkON で draw_graph() 再描画。

数学チャンネル(_build_tab_advanced 上段)

  • 「演算」math_op = mathchan.BINARY_OPS + mathchan.UNARY_OPSBINARY_OPS=["A+B","A-B","A×B","A÷B"]UNARY_OPS=["積分 ∫A dt","微分 dA/dt","絶対値 |A|","二乗 A²","移動平均","ローパス(RC)"]
  • A=math_a, B=math_b_on_math_op_change: B は二項演算時のみ有効)。パラメータ=math_param(既定"5"、ツールチップ「移動平均=窓長(点)、ローパス=カットオフ[Hz]」;needs_param は op∈("移動平均","ローパス(RC)") のみ有効)。
  • ボタン「数学チャンネルを作成」create_math_channel: A 未選択で情報。二項なら B も必須。二項 mathchan.binary(ta,ya,tb,yb,op)、単項 mathchan.unary(ta,ya,op,param=parse_eng(math_param,None))。例外で「演算エラー」。結果を新データセット label="Math: {op}"(重複なら「 ({i})」)、DataFrame({"時間[s]":x, op:r})meta={"path":label,"enc":"-","delim":"-"}_add_file_item_refresh_columns

FFT

  • show_fft()(メニュー/ボタン): _analysis_xy で対象。窓=fft_window.currentText()(既定"hann")、use_db=fft_db.isChecked()analysis.fft_spectrum(t,y,window)(freqs,amp)(None で警告)。analysis.find_spectral_peaks(t,y,n=npeaks)disp_amp=to_db(amp) if use_db else amp、ylab「振幅 [dBV]」/「振幅」。_reset_figure_axes()plot_series で1系列(color #1f77b4, lw1.0)を「折れ線」、title「FFTスペクトル: {label}({win}窓)」、xlabel「周波数 [Hz]」、grid True、legend False、markers にピーク(text「第{rank}\n{freq:.0f}Hz」, color #ff3030)。peak_table を周波数表示(「{freq:.4g} Hz」「{amplitude:.4g}」)に更新。_has_drawn は更新されない(draw_graph 未経由)点に注意。

高度解析タブ(その他)

  • FFT詳細: 「窓関数」fft_window=analysis.WINDOWS=["hann","hamming","blackman","flattop","rect"](既定 hann)。「dB表示」fft_db。ボタン「THD/SNR等を計算」compute_fft_metricsanalysis.spectrum_metrics(t,y,window)fft_metrics 表(列「指標/値」、maxHeight170)に行: 基本波 f0[Hz], THD[%], THD[dB], SNR[dB], SINAD[dB], ENOB[bit], SFDR[dB](None は"-"、それ以外「{val:.4g} {unit}」、キー f0/THD_pct/THD_dB/SNR_dB/SINAD_dB/ENOB_bits/SFDR_dB)。ボタン「スペクトログラム」show_spectrogramanalysis.spectrogrampcolormesh(cmap="viridis")+colorbar(label="dB")、xlabel「時間 [s]」ylabel「周波数 [Hz]」、_has_drawn=False
  • マスク/アイ/ジッタ: 「上限」mask_upper/「下限」mask_lower(placeholder「なし」)。ボタン「マスク判定」run_mask_test: 両空で情報。advanced.mask_test(t,y,upper,lower)draw_graph() 後に上限/下限の axhline(color #d00,ls--)res["violations"] あれば違反点を "." color #d00 ms3res["passed"] で「PASS ✅」/「FAIL ❌({violations}点 超過)」を adv_result に。res キー: passed/violations/violation_times/mask。
    • 「シンボルレート[Hz]/周期[s]」eye_rate(既定"1e6")。ボタン「アイダイアグラム」show_eye_diagram: val=parse_eng(eye_rate,1e6)sym_period = 1/val if val>1 else (val or 1e-6)advanced.eye_diagram(t,y,sym_period,n_ui=2)plot(phase*1e6, yy, "." ms0.5 alpha0.3 #1f77b4)、xlabel「UI内時間 [µs]」ylabel「電圧」、_has_drawn=False
    • ボタン「ジッタ解析(TIE)」run_jitter: advanced.jitter_tie(t,y)(falsy で警告)。結果 adv_result「ジッタ: RMS={…}s pp={…}s クロック≈{…}Hz エッジ{edges}本」(キー rms/pp/freq/edges、format_eng)。
  • プロトコル解読: 「プロトコル」proto_combo=["UART","I2C","SPI"]。「ボーレート/不使用」proto_baud(既定"115200")。Ch1–Ch3 proto_ch[0..2](コンボ、選択Y系列で同期)。_on_proto_change の設定: UART=ラベル(信号線,−,−)/baud有効/"115200"、I2C=(SCL,SDA,−)/baud無効、SPI=(SCK,MOSI,CS(任意))/baud無効。ラベルが""のChは非表示。
    • ボタン「解読」decode_protocol: Ch1 未選択で情報。UART=advanced.decode_uart(t1,y1,baud=parse_eng(proto_baud,115200))、行(time,"data",hex,char+(ok?:" ⚠"))。I2C=Ch2(SDA)必須、decode_i2c(t1,y1,y2)、START/STOP は値/備考空、他は(time,type,hex,"{rw} {ack}")。SPI=Ch2(MOSI)必須、Ch3(CS)任意、decode_spi(t1,y1,y2,cs)、行(time,"data",hex,"")。
    • proto_table(4列)ヘッダ「時刻/種別/値(hex)/備考」。時刻は「{time*1e3:.4g} ms」。例外で「解読エラー」。ステータス「{proto} 解読: {n} 件」。

縦横比(_aspect_ratio/_apply_aspect

  • aspect_combo 項目: ["自動(画面に合わせる)", "16:9", "4:3", "3:2", "1:1", "9:16(縦)", "A4横", "A4縦", "カスタム"]
  • 「カスタム W:H」= aspect_w/aspect_h(SpinBox 1–100、既定16/9)。_on_aspect_changed: カスタム時のみ W/H 有効化、_request_redraw
  • _aspect_ratio(): presets = {16:9→(16,9),4:3→(4,3),3:2→(3,2),1:1→(1,1),"9:16(縦)"→(9,16),A4横→(297,210),A4縦→(210,297)}。カスタムは (W,H)。返り値は box aspect = h/w(自動/その他は None)。
  • _apply_aspect(): ax.set_box_aspect(ratio)、第2軸 ax._twin_secondary があればそれにも適用(例外無視)。画面表示・画像出力・一括出力すべてに反映。

一括出力(batch_export)

  • データ無しで情報。テンプレート=選択中Y系列の「列名」(順序保持・重複除去)と列ごとの代表スタイル style_by_col。列名無しなら情報「Y軸(値)の系列を1つ以上選んでください。…」。
  • _batch_options_dialog()(ダイアログ「一括画像保存の設定」): 「グラフタイトル」(既定 title_edit.text() or "{name}"{name}=拡張子なしファイル名に置換)、「出力形式」=["png","jpg","pdf","svg"]、「解像度 DPI」(50–1200 step50、既定 dpi_spin の現値)、「背景透過」。OK ボタンのラベルは「フォルダを選んで保存...」。戻り {"title","fmt","dpi","transparent"}、キャンセルで None。
  • ダイアログ後 getExistingDirectory でフォルダ選択(未選択で中止)。
  • 各ファイルごとに 別 Figurematplotlib.figure.Figure(figsize=画面fig と同じget_size_inches, dpi)FigureCanvasAgg)に描画し画面を破壊しない。_build_series_for_file(label,x_name,cols,ctype,style_by_col) で系列生成(leftmost ON 時は各ファイル先頭列を位置でX軸)。title は title_tpl.replace("{name}", stem)、xlabel は xlabel_edit.text() or (先頭列名 if leftmost else x_name)ratio/fonts/xlim/ylim/trend/data_labels/max_points(=DECIMATE_TARGET if decimate ∧ 折れ線/散布図 else 0)を適用。
  • 対象列なし(cols 空)はスキップskipped.append("{label}(対象列なし)"))。ファイル名衝突は _safe_filename[\\/:*?"<>|]+→"_")+ 連番 _{k}。保存 fig.savefig(path,dpi,bbox_inches="tight",transparent)
  • 完了で QMessageBox.information("一括出力", "…{len(saved)} 件…\n{out_dir}…スキップ {n} 件: 先頭5件 …ほか")last_dir=out_dir

画像出力(save_figure / copy_figure)

  • 「解像度 DPI」=dpi_spin(50–1200 step50、既定150)。「背景透過」=transparent_check
  • save_figure: フィルタ「PNG (.png);;JPEG (.jpg);;PDF (.pdf);;SVG (.svg);;EPS (*.eps)」。既定名 graph.pngfig.savefig(path,dpi,bbox_inches="tight",transparent)
  • copy_figure: PNG を io.BytesIO に savefig→QImage.fromDataQApplication.clipboard().setImage。Null で「画像の生成に失敗しました。」。

設定の保存/復元(_collect_config / _apply_config)

  • _collect_config() の返却キー: files(=各 meta path), x_col, x_leftmost, selected_y(=[[fl,col],…]), chart_type, title, xlabel, ylabel, fonts({title,label,tick}), grid, legend, legend_loc, xmin, xmax, ymin, ymax, xlog, ylog, bins, pct, trend, trend_degree, trend_window, trend_eq, data_labels, aspect, aspect_w, aspect_h, export_dpi, transparent, recent_files, styles(=series_styles), scope(=_scope_dict), npeaks
  • _apply_config(cfg, load_files=True): _suspend_redraw 中に _apply_config_inner を実行。復元の既定値: chart_type"折れ線", fonts(12/10/9), grid/legend True, legend_loc"best", bins30, pct True, trend"なし", trend_degree2, trend_window5, trend_eq True, data_labels False, aspect"自動(画面に合わせる)", aspect_w16/h9, export_dpi150, transparent False, scope(enabled False, tdiv=format_eng(t_per_div or 1e-3)+"s", vdiv=format_eng(v_per_div or 0.5), x_divs10, y_divs8), npeaks5。Y選択は (fl,col) で照合復元。recent_files は文字列のみ先頭12件。
  • save_config_dialog/load_config_dialog: config_io.save_config/load_config(JSON、フィルタ「JSON (*.json)」、既定名 graph_config.json)。読込後 _apply_configdraw_graph
  • 終了時自動保存/起動時復元: __init___try_restore_session()config_io.load_last_session()files 無しで False;復元成功時データありで draw_graph()、ステータス「前回のセッションを復元しました。」)。closeEventconfig_io.save_last_session(_collect_config())(例外無視)。
    • config_io: 自動保存先 ~/.csv_graph_tool/last_session.jsonAPP_DIR)。save_config_version=CONFIG_VERSION(=1) を付与し UTF-8/indent2 で JSON 保存。load_config 失敗時 None。

既知の制約・回避策

  • _shift_held は matplotlib のスクロール時 Shift 取りこぼし対策で Qt 修飾キーを優先。
  • スペクトログラム/アイ/トレンド表示後は _has_drawn=False にして、次の draw_graph でカラーバー付き特殊軸を作り直す。
  • _reset_figure_axesself.ax 以外の Figure 軸(カラーバー等)を除去。系列バーの表示制御は _rebuild_series_bar のみが行う。
  • デシメーションのズーム再サンプル(_setup_dynamic_resample)は 折れ線かつ数値X(有限率≥0.8) のみ対象。xlim_changed で 120ms デバウンス、表示範囲±5%マージンを decimate_minmax で再サンプル。
  • リアルタイム再描画は _request_redraw_suspend_redraw 偽 ∧ _has_drawnlive_checkON)で 180ms デバウンス。

参照定数(plotter.py)

  • CHART_TYPES=["折れ線","棒","横棒","積み上げ棒","散布図","ヒストグラム","箱ひげ","円"]
  • LINESTYLES={"実線":"-","破線":"--","一点鎖線":"-.","点線":":","なし":"None"}
  • MARKERS={"なし":"","丸":"o","四角":"s","三角":"^","菱形":"D","× ":"x","+":"+","点":"."}
  • LEGEND_LOCS=["best","upper right","upper left","lower left","lower right","right","center left","center right","lower center","upper center","center"]
  • TRENDLINES=["なし","線形","多項式","指数","対数","移動平均"]
  • SERIES_KINDS={"自動":"","折れ線":"line","棒":"bar","面":"area","散布図":"scatter"}SERIES_AXES={"主軸":"primary","第2軸":"secondary"}
  • parse_eng('1ms'/'500us'/'1e-3' 等→float)、format_engeng_125_sequence(lo,hi,suffix)(1-2-5 系列文字列)、decimate_minmax

8. 受け入れ基準(実装後に自動テストで検証する)

offscreen(QT_QPA_PLATFORM=offscreen)でGUIを生成し、最低限つぎを満たすこと:

  1. アプリが例外なく起動・構築できる(3ペイン、左3タブ、右に書式コントロール+8列スタイル表)。
  2. CSV/TSV読込:文字コード・区切りの自動判定が効く(cp932の日本語が化けない)。列名重複は一意化。
  3. 8種グラフ(折れ線/棒/横棒/積み上げ棒/散布図/ヒストグラム/箱ひげ/円)が描ける。複数ファイル重ね描き(折れ線/散布図/ヒストグラム/箱ひげ)。
  4. Excel相当編集:系列の色/線種/幅/マーカー、タイトル/軸名/文字サイズ、グリッド/凡例(位置)、軸範囲(片側可)/対数、
    近似曲線(線形/多項式/指数/対数/移動平均+数式R²)、データラベル、第2軸・複合(系列ごと軸/種別)、エラーバー。
  5. データのセル編集→DataFrame反映、行追加/行削除/列追加、CSV保存(全行)。
  6. オシロ表示:time/div・V/div(1-2-5+単位)、位置、divグリッド、マウス操作(左ドラッグ位置/右ドラッグスケール/
    ホイールtime÷div/Shift+ホイールV/div)、カーソル測定(Δt/ΔV/1÷Δt・ドラッグ微調整)、ピーク第1/第2+平滑化、各種測定。
  7. 数学チャンネル/FFT/高度解析(マスク/アイ/ジッタ/UART・I2C・SPI解読)。
  8. 縦横比固定(自動/16:9/4:3/3:2/1:1/9:16/A4横/A4縦/カスタム)が画面・出力に反映。
  9. 一括画像出力:選択Y列名をテンプレに各ファイル1枚、タイトル{name}置換/形式png-jpg-pdf-svg/DPI/透過を設定ダイアログで調整、
    別Figure描画で画面非破壊、対象列なしはスキップ。500ファイルでも完走。
  10. 「一番左の列をX軸にする(位置で固定)」ON時:各ファイルの先頭列をX軸にし、その列をY候補から除外。
  11. ファイル一覧:複数選択・一括削除・全削除、長い名前は横スクロール/省略なし、縦幅をスプリッタで調整可。
  12. 設定の保存/復元(終了時自動保存・起動時復元)。Y未選択描画でポップアップを出さない。

9. 注意(このプロンプトの限界)

このプロンプトはシステムを仕様レベルで完全再現するためのものです。関数名・定数値・UIラベル・既定値・
挙動まで含めて忠実に再構築できますが、バイト単位で同一のソースを得るには元のソースファイルが必要です
(同じ仕様でも実装の細部は再構成されます)。完全同一が必要なら、ソース一式(上記ファイル)をそのまま配布してください。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?