再現用マスタープロンプト: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.FigureをFigureCanvasQTAggで埋め込む。Figure(figsize=(6,4.4), dpi=100)を初期値、
ax = fig.add_subplot(111)。ナビゲーションツールバーも設置。 -
numpy 2.x 対応:
np.trapzはnp.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)に左から:-
左:
QTabWidget(1. データ/2. オシロ/解析/3. 高度解析の3タブ。最小幅220)。 -
中央:
QSplitter(Vertical)= 上「グラフ表示(ツールバー+系列ON/OFFバー+キャンバス)」、下「データ編集(先頭100行・編集可)」。 - 右端: 「グラフ書式調整」パネル = 上段スクロール内にグラフ書式コントロール、下段に系列スタイル表(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.0matplotlib>=3.7PySide6>=6.5numpy>=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.を表示し、pause後exit /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"
- Windows:
関数:
-
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(判定手順を厳密に順序通り):
- ファイルをバイナリ全読み(
raw)。 - BOM 判定:
b"\xef\xbb\xbf"始まりなら"utf-8-sig"。b"\xff\xfe"またはb"\xfe\xff"始まりなら"utf-16"。 -
raw.decode("utf-8")が成功すれば"utf-8-sig"を返す(UTF-8 厳格チェック。成功時は utf-8 ではなく utf-8-sig を返す点に注意)。 - 日本語候補
("cp932", "euc-jp")を順に decode。先頭 100000 文字text[:100000]を_japanese_score。bad == 0 and jp > 0を満たす中でjp最大のものをbest_jpとして選び、あれば返す。 - charset-normalizer のヒント(
from charset_normalizer import from_bytes、from_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)。
- 判定対象名
- 欧文・その他フォールバック:
("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_encoding、delimiter=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_DIR。os.makedirs(APP_DIR, exist_ok=True)でフォルダ作成。 -
save_config(config, path)→path。data = 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.load。OSError/ValueError(JSON 不正含む)時はNone。 -
save_last_session(config)→path | None。ensure_app_dir()後にsave_config(config, LAST_CONFIG)。OSError時はNone。 -
load_last_session()→dict | None。LAST_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>0、pos.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>0、pos.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)/w、yf=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_tot(ss_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] 使用)。
処理順:
-
info=CHART_INFO.get(chart_type)、None ならValueError(f"未知のグラフ種別です: {chart_type}")。series空ならValueError("Y軸(値)の系列を選択してください。")。fonts = fonts or {}。 - 初期化:
ax.clear()→_remove_twin(ax)(前回第2軸掃除)→ax.set_aspect("auto")(円の equal 持越し回避)→ax.set_facecolor("white")(オシロ暗背景持越し回避)→ax.tick_params(colors="black")(目盛色既定へ)。 - 第2軸(折れ線/散布図のみ): いずれかの系列が
axis=="secondary"ならax2 = ax.twinx()、ax._twin_secondary = ax2。secondary_labelがあればax2.set_ylabel(secondary_label, fontsize=(fonts or {}).get("label",10))。 - 種別分岐:
- 折れ線/散布図 →
_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)
- 折れ線/散布図 →
- タイトル/ラベル: 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))。 - 対数軸・軸範囲(円以外):
xlog/ylogでset_xscale/set_yscale("log")。xlim/ylim は片側だけでも適用(xlim[0] is not Noneでset_xlim(left=...)、xlim[1] is not Noneでset_xlim(right=...)、ylim も bottom/top で同様)。コメント: 旧仕様で両方そろわないと無視され「min=0 でも余白が残る」原因だった、を解消。 - オシロ表示:
scope and scope.get("enabled") and chart_type in ("折れ線","散布図")のとき_apply_scope(ax, scope)を呼びgrid=Trueに強制。 - 凡例/グリッド: 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)。 - マーカー注記:
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))。 -
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に蓄積。カテゴリ位置は全系列で共有(系列ごとに目盛りを上書きして取り違える不具合の回避)。 - 棒の本数:
prepared中sr.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。
- line →
- 描画分岐:
-
bar:
w=_bar_width(xx)/n_bars、off=(bar_idx-(n_bars-1)/2)*w、bar_idx+=1。target.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)。
-
bar:
-
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_bars。bottom += 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*tpd、y0,y1 = yc ∓ yd/2*vpd。set_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**dec、lo*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_df 後 plot_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 = True、except ExceptionでFalse(簡易ピーク検出にフォールバック)。 - 多くの scipy 利用関数は関数内で遅延 import し、失敗時はフォールバックする方針。
sampling_rate(t)
-
t.size < 2→None。 -
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_filter→savgol_filter(y, w, min(2, w-1))(多項式次数 = 2 と w-1 の小さい方)。 - 失敗時: 移動平均
k = np.ones(w)/w、np.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 -y(mode="min"で谷検出)。 -
span = nanmax(y) - nanmin(y)、prom = prominence_frac*span if span>0 else None。 - scipy 有り:
find_peaks(sig, **kwargs)。promがあればprominence=prom、distanceがあれば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-base。span非有限 or<=0→ None。 -
y_lo = base + lo*span,y_hi = base + hi*span(10%/90% 既定)。 -
up_cross(level):below=y<levelのbelow[:-1] & ~below[1:]。down_cross(level):above=y>levelのabove[:-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"(nameNone→hann)。hamming→np.hamming,blackman→np.blackman,flattop→scipy.signal.windows.flattop(失敗時np.hanning),none/rect/rectangular→np.ones(n), それ以外→np.hanning(n)。- 注意: "hann" は分岐に無く
np.hanning(n)の既定経路で処理。
- 注意: "hann" は分岐に無く
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*w、spec = 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_spectrumのamp[1:]/freqs[1:](DC 除外)にfind_signal_peaks(mode="max")を適用。 - 戻り値:
list of dict {rank, frequency, amplitude}(内部 dict のtime→frequency、value→amplitudeに詰め替え)。
measurements(t, y) — 戻り値: list of {name, value, unit}(この順序で append)
-
最大値 Vmax, V —nanmax(y)(有限値無ければ全項 None) -
最小値 Vmin, V —nanmin(y) -
P-P値 Vpp, V —vmax-vmin -
平均 Vmean, V —nanmean(y) -
実効値 Vrms, V —sqrt(nanmean(y**2)) -
標準偏差 σ, V —nanstd(y) -
周期, s —_zero_crossing_period -
周波数(ゼロ交差), Hz —1/period -
周波数(FFT), Hz —dominant_frequency -
Top, V —histogram_top_baseの top -
Base, V — base -
振幅 Vamp(Top-Base), V —top-base -
オーバーシュート, % —amp>0のとき(vmax-top)/amp*100、それ以外 None -
アンダーシュート, % —(base-vmin)/amp*100、それ以外 None -
立上り時間 (10-90%), s —_edge_time(rising=True) -
立下り時間 (90-10%), s —_edge_time(rising=False) -
+パルス幅, s —pulse_metrics.pos_width -
-パルス幅, s —neg_width -
+デューティ比, % —pos_duty -
-デューティ比, % —neg_duty -
立上りエッジ数, (無単位)—rising_edges -
サイクル数, (無単位)—cycles -
立上り→立上り, s —edge_intervals.rise_to_rise -
立下り→立下り, s —fall_to_fall -
立上り→立下り(High幅), s —rise_to_fall -
立下り→立上り(Low幅), s —fall_to_rise -
Time@Max, s —t[nanargmax(y)](有限値無ければ None) -
Time@Min, s —t[nanargmin(y)] -
面積 ∫y dt, V·s —t,yともに有限なマスクfinで_trapz(y[fin], t[fin])(有限点 < 2 で None) -
サンプル数, 点 —float(y.size) -
サンプリング周波数, Hz —sampling_rate(t)
edge_intervals(t, y, level=None) — 戻り dict(条件成立した key のみ)
- 有限値 < 4 →
{}。 -
level未指定ならhistogram_top_baseの(top+base)/2。top 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)、ビン中心centers、mid=(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)/2(top<=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))*w、spec=rfft、power=|spec|**2。power.size<4or 全非有限 →{}。 - 基本波ビン
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]=0、spur.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.spectrogramimport 失敗 →(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.pyはimport 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 dt、dA/dt、|A|、A²(上付き2)に注意。
- 文字列はこの正確なラベルで一致判定される(分岐は完全一致)。
1.2 内部ヘルパー _sorted_xy(x, y)
-
x, yをfloatの 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。 - 処理:
-
xa, yaを float ndarray 化。 - B を
_sorted_xy(xb, yb)で x 昇順ソート。 -
yb_i = np.interp(xa, xbs, ybs)で B を A の時間軸 xa に線形補間(A の各点での B の値)。np.interpの既定挙動上、xa が xb 範囲外なら端点値でクランプされる。 -
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 化される)、演算名op、param(移動平均の窓長 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化)、交差レベルlevel、edge("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 None→viol |= y > upper;lower is not None→viol |= 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_period、n_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, y、threshold(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-1でbt = 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=False。pposを 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解読
- 引数:
t、scl、sda、threshold(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ビット目をack。valを 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[...])。
- START:
2.10 decode_spi(t, sck, mosi, cs=None, threshold=None, cpol=0, cpha=0, bits=8, msb_first=True) — SPI解読
- 引数:
t、sck、mosi、cs(任意, 既定None)、threshold(None→auto_threshold(np.concatenate([sck, mosi])))、cpol(既定0)、cpha(既定0)、bits(既定8)、msb_first(既定True)。 - ロジック化:
Lsck, Lmosi、Lcs = _logic(cs, th) if cs is not None else None。 - サンプルエッジ決定:
rising/fallingを求め、sample_edges = rising if (cpol==cpha) else falling、np.sort。 - 各サンプルエッジで:
-
Lcsがあり該当 index でLcs==1(CS=High=非選択)→ ビットをリセットしスキップ(cur,nbits,start_idx = 0,0,None)。 -
start_idx未設定なら設定。bit = _level_at(t, Lmosi, t[i])。 -
msb_first→cur = (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_diagramはsymbol_period<=0で(None, None)。 -
binaryのA÷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 NavigationToolbarfrom matplotlib.figure import Figure- 自作モジュール:
advanced, analysis, config_io, data_loader, jp_font, mathchan, plotter
-
- モジュール定数:
PREVIEW_ROWS = 100UserRole = 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, なし
- 「使い方」→
-
ファイル(&F):
3. _build_central(3ペインレイアウト)
- 中央ウィジェット内に
QHBoxLayout(contentsMargins 6,6,6,6)→ 横方向QSplitter。 -
左ペイン:
QTabWidget(self.tabs、setMinimumWidth(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(データタブ)
- 全体:
QWidget内QVBoxLayout(margins 0)→ 縦QSplitter(vsplit)。上段=ファイル一覧、下段=読込設定/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: 縦QSplitter(setMinimumWidth(360))。- 上段:
QScrollArea(setWidgetResizable(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=None、self._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]=df、self.meta[label]={"path","enc":used_enc,"delim":used_delim}、一覧未登録なら_add_file_item(label)、_push_recent(path)、ステータス「{label} を読み込み({行}行 × {列}列, {used_enc})」。
- 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)をsetdefault。DEFAULT_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 線種:
QComboBox(plotter.LINESTYLESのキー)。LINESTYLES={"実線":"-","破線":"--","一点鎖線":"-.","点線":":","なし":"None"}。変更→_set_style(key,"linestyle",値)。 - 列3 幅:
QDoubleSpinBoxrange(0.2,10) step 0.5、初期=st["linewidth"]。→_set_style(...,"linewidth",v)。 - 列4 マーカー:
QComboBox(plotter.MARKERSキー)。MARKERS={"なし":"","丸":"o","四角":"s","三角":"^","菱形":"D","× ":"x","+":"+","点":"."}。 - 列5 軸:
QComboBox(plotter.SERIES_AXES)。SERIES_AXES={"主軸":"primary","第2軸":"secondary"}、既定 "主軸"。 - 列6 種別:
QComboBox(plotter.SERIES_KINDS)。SERIES_KINDS={"自動":"","折れ線":"line","棒":"bar","面":"area","散布図":"scatter"}、既定 "自動"。 - 列7 誤差列:
QComboBox、先頭 "なし" + 同ファイルの全列名。→_set_style(...,"errcol", None if "なし" else v)。
- 列0 系列名:
-
_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_drawnでdraw_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_qtagg の FigureCanvasQTAgg/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(描画フロー)
-
self.datasetsが空 → 情報ダイアログ「先にファイルを追加してください。」で return。 -
Y系列未選択(
_selected_series_items()空)はポップアップを出さず、_draw_placeholder()→_rebuild_series_bar(現在の種別)→ ステータス「Y軸(値)の系列をチェックするとグラフを表示します。」で return。 -
ctype = self.chart_combo.currentText()。issues=[]警告蓄積リスト。 -
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)を返す。 -
scope=self._scope_dict()。scope["enabled"]かつctype in ("折れ線","散布図")でt_per_div/v_per_divのどちらかが正でなければ「time/div・V/div は正の値が必要(オシロ表示を無効化)」を追加しenabled=Falseに。 -
_clear_dynamic_resample(),_reset_figure_axes()(メイン軸self.ax以外=カラーバー軸等をremove()), カーソル状態(_cursor_pts/_cursor_artists/_cursors/_cursor_drag/_cursor_text)をリセット。 -
series, categories = _build_series(ctype)。 - Y対数ON かつ Y系列に0以下があれば「Y対数: 0以下の値は表示されません」を追加。X対数ON かつ折れ線/散布図でX系列に0以下があれば「X対数: 0以下の値は表示されません」を追加(
_has_nonpositive:pandas で数値化→有限値の最小が ≤0 か)。 -
デシメーション:
total = 全系列の len(y) 合計。max_points = DECIMATE_TARGET if (decimate_check.isChecked() and ctype in ("折れ線","散布図") and total>DECIMATE_TARGET) else 0。 -
busy = total > BUSY_ROWSのとき待機カーソル+ステータス「描画中…({total:,} 点)」。 -
markers = _peak_markers() if show_peaks_check.isChecked() else None。sec_label = 第2軸系列ラベルを " / " 連結。 -
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。 -
_apply_aspect()→(縦横比固定)→fig.tight_layout()(例外無視)→canvas.draw()。 -
max_pointsありなら_setup_dynamic_resample(series,ctype,max_points)。 -
_rebuild_series_bar(ctype)。_plotted_artistsを「ラベルに『近似』を含まないax.get_lines()」から[(label, line)]で再構築(カーソル追従用)。_has_drawn=True。 - ステータス「『{ctype}』を描画しました(系列 {n})。」+デシメート時「({total:,}点を間引き表示)」+issues あれば「 ⚠ 」+
" / "連結。 - 例外時は
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_bar(QScrollArea、setFixedHeight(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.NoSelection。itemChanged→_on_y_check_changed、itemDoubleClicked→_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・黒半透明背景。
- 左ドラッグ(button1 ∧ not shift)= パン(位置移動)。px 移動量を data 単位に換算して
-
_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_OPS。BINARY_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_metrics→analysis.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_spectrogram→analysis.spectrogramのpcolormesh(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 ms3。res["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)。
- 「シンボルレート[Hz]/周期[s]」
-
プロトコル解読: 「プロトコル」
proto_combo=["UART","I2C","SPI"]。「ボーレート/不使用」proto_baud(既定"115200")。Ch1–Ch3proto_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でフォルダ選択(未選択で中止)。 - 各ファイルごとに 別 Figure(
matplotlib.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.png。fig.savefig(path,dpi,bbox_inches="tight",transparent)。 -
copy_figure: PNG をio.BytesIOに savefig→QImage.fromData→QApplication.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_config→draw_graph。 -
終了時自動保存/起動時復元:
__init__で_try_restore_session()(config_io.load_last_session()、files無しで False;復元成功時データありでdraw_graph()、ステータス「前回のセッションを復元しました。」)。closeEventでconfig_io.save_last_session(_collect_config())(例外無視)。-
config_io: 自動保存先~/.csv_graph_tool/last_session.json(APP_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_axesはself.ax以外の Figure 軸(カラーバー等)を除去。系列バーの表示制御は_rebuild_series_barのみが行う。 - デシメーションのズーム再サンプル(
_setup_dynamic_resample)は 折れ線かつ数値X(有限率≥0.8) のみ対象。xlim_changedで 120ms デバウンス、表示範囲±5%マージンをdecimate_minmaxで再サンプル。 - リアルタイム再描画は
_request_redraw(_suspend_redraw偽 ∧_has_drawn∧live_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_eng、eng_125_sequence(lo,hi,suffix)(1-2-5 系列文字列)、decimate_minmax。
8. 受け入れ基準(実装後に自動テストで検証する)
offscreen(QT_QPA_PLATFORM=offscreen)でGUIを生成し、最低限つぎを満たすこと:
- アプリが例外なく起動・構築できる(3ペイン、左3タブ、右に書式コントロール+8列スタイル表)。
- CSV/TSV読込:文字コード・区切りの自動判定が効く(cp932の日本語が化けない)。列名重複は一意化。
- 8種グラフ(折れ線/棒/横棒/積み上げ棒/散布図/ヒストグラム/箱ひげ/円)が描ける。複数ファイル重ね描き(折れ線/散布図/ヒストグラム/箱ひげ)。
- Excel相当編集:系列の色/線種/幅/マーカー、タイトル/軸名/文字サイズ、グリッド/凡例(位置)、軸範囲(片側可)/対数、
近似曲線(線形/多項式/指数/対数/移動平均+数式R²)、データラベル、第2軸・複合(系列ごと軸/種別)、エラーバー。 - データのセル編集→DataFrame反映、行追加/行削除/列追加、CSV保存(全行)。
- オシロ表示:time/div・V/div(1-2-5+単位)、位置、divグリッド、マウス操作(左ドラッグ位置/右ドラッグスケール/
ホイールtime÷div/Shift+ホイールV/div)、カーソル測定(Δt/ΔV/1÷Δt・ドラッグ微調整)、ピーク第1/第2+平滑化、各種測定。 - 数学チャンネル/FFT/高度解析(マスク/アイ/ジッタ/UART・I2C・SPI解読)。
- 縦横比固定(自動/16:9/4:3/3:2/1:1/9:16/A4横/A4縦/カスタム)が画面・出力に反映。
- 一括画像出力:選択Y列名をテンプレに各ファイル1枚、タイトル{name}置換/形式png-jpg-pdf-svg/DPI/透過を設定ダイアログで調整、
別Figure描画で画面非破壊、対象列なしはスキップ。500ファイルでも完走。 - 「一番左の列をX軸にする(位置で固定)」ON時:各ファイルの先頭列をX軸にし、その列をY候補から除外。
- ファイル一覧:複数選択・一括削除・全削除、長い名前は横スクロール/省略なし、縦幅をスプリッタで調整可。
- 設定の保存/復元(終了時自動保存・起動時復元)。Y未選択描画でポップアップを出さない。
9. 注意(このプロンプトの限界)
このプロンプトはシステムを仕様レベルで完全再現するためのものです。関数名・定数値・UIラベル・既定値・
挙動まで含めて忠実に再構築できますが、バイト単位で同一のソースを得るには元のソースファイルが必要です
(同じ仕様でも実装の細部は再構成されます)。完全同一が必要なら、ソース一式(上記ファイル)をそのまま配布してください。