Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 80 additions & 21 deletions AGENTS.md

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,26 @@ https://mioupa.github.io/matplotlib_gui/ を開いてください。`index.html`
- 軸の対数表示と範囲指定、余白指定、主目盛線・副目盛線、凡例位置
- 保存: png / jpg / svg / pdf、背景透過
- データ確認タブ: 先頭100行を表示し、描画から除外される先頭行をグレーで表示
- Pythonコード(beta)タブ: 描画用コードの確認と実行(Phase 2 で作り替え予定)
- Pythonコードタブ: 設定から生成した matplotlib のスクリプトを表示(下記)
- 数値に変換できず除外した値の警告、日本語のエラーメッセージ

## Pythonコードタブ

図は、設定から生成した Python スクリプトの実行で描いています。「Pythonコード」タブには、そのスクリプト全体(データの読み込みから保存まで)が表示されます。

- 設定を変えると、スクリプトも図と一緒に更新されます(「GUI と同期しています」)。
- 「コピー」でクリップボードへコピーし、「.py で保存」でファイルに書き出せます。ファイル名は「保存設定」のファイル名に `.py` を付けたものです。
- 「編集」を押すと、スクリプトを書き換えられます。「実行」(または Ctrl / Cmd + Enter)でそのコードを実行し、図を更新します。編集中は GUI の変更を図にもコードにも反映しません(「GUI と同期していません」)。「GUI から再生成」で、現在の設定から作ったコードに戻ります。
- `print` の出力と、エラー時の行番号付きトレースバックは、タブ内の「実行結果」に表示されます。

### 書き出した .py をローカルで実行する

1. 書き出した `.py` を、データファイルと同じフォルダに置きます(スクリプトは元のファイル名でデータを読みます)。
2. `pip install pandas matplotlib numpy` を実行します。`.xlsx` を読むときは `openpyxl` も入れます。
3. `python plot.py` で実行します(ファイル名は書き出した名前に読み替えてください)。

日本語フォントは、見つかったものを使います。無い場合も実行はできますが、日本語は豆腐(□)で表示されます。

## プライバシー

読み込んだデータの処理はすべてブラウザの中で行い、外部には送信しません。外部への通信は、ライブラリ(PyScript / Pyodide / pandas / matplotlib など)とフォントの取得だけです。通信先は次のとおりです。
Expand Down
168 changes: 145 additions & 23 deletions css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,28 +159,56 @@ button {
background: var(--card);
color: var(--text);
}
input[type="file"] {
.file-picker {
position: relative;
display: flex;
align-items: center;
gap: 10px;
padding: 6px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
font-size: 14px;
}
input[type="file"]::file-selector-button {
border: none;
.file-picker:focus-within {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.file-picker-btn {
flex: none;
border-radius: 6px;
padding: 8px 12px;
margin-right: 10px;
background: var(--file-button-bg);
color: var(--file-button-text);
font-weight: 700;
cursor: pointer;
white-space: nowrap;
}
.file-picker:hover .file-picker-btn {
background: var(--button-ghost-hover);
}
input[type="file"]::-webkit-file-upload-button {
.file-picker-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text);
}
.file-picker-name[data-selected="false"] {
color: var(--text-muted);
}
.file-picker input[type="file"] {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 0;
border: none;
border-radius: 6px;
padding: 8px 12px;
margin-right: 10px;
background: var(--file-button-bg);
color: var(--file-button-text);
font-weight: 700;
border-radius: 8px;
opacity: 0;
cursor: pointer;
}
.file-picker input[type="file"]::file-selector-button {
cursor: pointer;
}
input[type="color"] {
Expand Down Expand Up @@ -463,11 +491,13 @@ button.secondary:hover {
margin-bottom: 8px;
font-size: 13px;
}
#codePanel {
overflow: auto;
}
.code-editor-wrap {
display: flex;
flex-direction: column;
gap: 10px;
height: 100%;
min-height: 0;
}
.code-toolbar {
Expand All @@ -476,25 +506,106 @@ button.secondary:hover {
flex-wrap: wrap;
gap: 8px;
}
.code-toolbar .inline {
margin-right: auto;
}
.code-toolbar button {
width: auto;
}
#customPyCode {
width: 100%;
height: 100%;
min-height: 360px;
resize: vertical;
.code-toolbar button[hidden] {
display: none;
}
.code-sync-badge {
margin-right: auto;
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 12px;
font-weight: 700;
color: var(--text-muted);
background: var(--surface);
}
.code-sync-badge[data-state="edit"] {
color: var(--status-warning);
border-color: var(--status-warning);
background: var(--warning-bg);
}
.code-editor-box {
display: flex;
height: clamp(260px, 52vh, 560px);
min-height: 0;
border: 1px solid var(--line);
border-radius: 10px;
overflow: hidden;
background: var(--surface);
}
.is-edit .code-editor-box {
border-color: var(--status-warning);
box-shadow: 0 0 0 1px var(--status-warning);
}
.code-gutter,
#customPyCode {
margin: 0;
padding: 12px 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
line-height: 20px;
white-space: pre;
}
.code-gutter {
flex: none;
min-width: 3.4em;
padding: 12px 8px 32px;
overflow: hidden;
text-align: right;
color: var(--table-rownum-text);
background: var(--table-rownum-bg);
border-right: 1px solid var(--line-soft);
user-select: none;
}
#customPyCode {
flex: 1;
min-width: 0;
width: auto;
padding: 12px;
border: none;
outline: none;
resize: none;
overflow: auto;
background: transparent;
color: var(--text);
tab-size: 4;
}
#customPyCode:focus-visible {
box-shadow: inset 0 0 0 2px var(--accent);
}
.code-output-title {
margin: 6px 0 0;
font-size: 13px;
font-weight: 700;
color: var(--text-muted);
}
.code-output {
margin: 0;
max-height: 260px;
min-height: 44px;
overflow: auto;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--surface);
color: var(--text);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.code-output:empty::before {
content: "(出力はありません)";
color: var(--text-muted);
}
.code-output[data-kind="error"] {
border-color: var(--error-border);
background: var(--error-bg);
color: var(--error-text);
}
.help-text {
margin-top: 6px;
Expand Down Expand Up @@ -653,7 +764,7 @@ button.series-color-chip.active {
overflow: auto;
}
.layout {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
height: auto;
}
.panel {
Expand All @@ -676,3 +787,14 @@ button.series-color-chip.active {
flex: none;
}
}
@media (max-width: 480px) {
.tabs {
gap: 6px;
}
.tab-btn {
flex: 1 1 auto;
min-width: 0;
padding: 8px 6px;
font-size: 13px;
}
}
43 changes: 43 additions & 0 deletions docs/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,46 @@ Phase 1 で行った改善:
ベースラインとの比較では、起動(コールド・ウォーム)、再描画、保存はいずれも悪い。移行直後よりは改善したが、ベースラインには戻っていない。要件(8章)の「起動時間を F6 の現状値より悪くしない」は満たしていない。初回ファイル読込 → 最初のプロットは良い(上の注記のとおり参考値)。

**決定(2026-10-08、オーナー)**: ウォーム起動に残る差(約 +0.5 秒)は実行環境(Pyodide 314 / Python 3.14)自体のコストとして許容する。以後の要件8章の基準値は、この「Phase 1 後」の値とする(docs/REQUIREMENTS.md 8章に注記済み)。

## Phase 2 後

### 計測条件

| 項目 | 内容 |
|---|---|
| 日付 | 2026-10-08 |
| 対象 | `88beb50`(Phase 2: 設定 → `dataprep.plan_plot` → `codegen.generate_script` → `runner` が生成スクリプトを実行)に、編集モード実行の計測行を追加した `tests/perf/bench.py` を組み合わせたもの。PyScript 2026.7.3 / Pyodide 314.0.3 |
| マシン | Mac16,10 / Apple M4 / メモリ 24 GB |
| OS | macOS 27.0.1 |
| ブラウザ | Chromium 153.0.8010.12(Playwright、ヘッドレス) |
| ネットワーク | 実ネットワーク(ベースラインと同じ) |
| 配信 | ローカルの静的 HTTP サーバー(127.0.0.1、サブパスなし) |
| 反復 | 各項目 N=5。計測方法は Phase 1 後と同じ(描画系の項目は 10,000 行 × 系列 5 本) |

追加した項目: 「編集モードでの実行(Ctrl/Cmd+Enter → プロット更新)」。同じ 10,000 行のファイルで、Pythonコードタブを編集モードにして `#customPyCode` で Ctrl/Cmd+Enter を押してから、新しい画像(`#plotArea img` の世代が進み、描画が idle)になるまで。ファイルを仮想 FS から読み直し、スクリプト全体を実行する。デバウンスは含まない。保存の計測の後に測るので、既存の項目には影響しない。Phase 1 後の値はない。

### 結果(ミリ秒、中央値 / 最小 / 最大)

| 項目 | Phase 1 後 | Phase 2 後 | Phase 1 後比 |
|---|---:|---:|---|
| コールド起動 | 3,650 / 3,593 / 6,581 | 3,607 / 3,578 / 6,407 | 同等(-1%) |
| ウォーム起動 | 3,462 / 3,432 / 3,469 | 3,452 / 3,440 / 3,458 | 同等(-0%) |
| 初回ファイル読込 → 最初のプロット | 186 / 184 / 187 | 178 / 175 / 181 | 良い(-4%) |
| 設定変更 → プロット更新(デバウンス込み) | 474 / 469 / 476 | 466 / 460 / 477 | 同等(-2%) |
| 同上(デバウンス 250ms 除く) | 224 / 219 / 226 | 216 / 210 / 227 | 同等(-4%、範囲が重なる) |
| 保存(PNG、クリック → ダウンロード開始) | 257 / 246 / 262 | 246 / 211 / 253 | 同等(-4%、最小側にばらつき) |
| 初回 .xlsx 読込 → 最初のプロット | 336 / 335 / 339 | 329 / 327 / 332 | 良い(-2%、範囲は重ならない) |
| 編集モードでの実行(Ctrl/Cmd+Enter → プロット更新) | (計測なし) | 438 / 431 / 464 | - |

コールド起動の最大値は1回目の反復のみ(キャッシュが冷えていたため)で、2回目以降は約3.6秒。

### 要件の確認

要件「設定変更後 1 秒以内の再描画(10,000 行 × 5 系列)」: 設定変更 → プロット更新は、デバウンス 250 ms を含めて中央値 466 ms(最大 477 ms)で、**満たしている**。編集モードの実行も中央値 438 ms(最大 464 ms)で 1 秒以内。

### 分析

Phase 1 後より悪化した項目はなかった。このため、悪化の内訳調査は行っていない。codegen 経由の描画(データ整形 → スクリプト生成 → 実行)は、Phase 1 の直接描画と同じ水準に収まっている。起動に変更はない(差は誤差の範囲)。

編集モードの実行(438 ms)は、設定変更の描画(デバウンス除く 216 ms)より約 220 ms 長い。仮想 FS からのファイル再読込と、生成スクリプトの全体実行(読込・整形・描画)を含むためとみられるが、内訳は未計測。短縮が必要になったら、Pyodide 内で読込・コンパイル・実行・PNG 変換の時間を分けて測ることから始める。

30 changes: 19 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ <h1>Matplotlib GUI</h1>

<div class="group">
<label for="fileInput">入力ファイル (.xlsx/.csv/.txt)</label>
<input id="fileInput" type="file" accept=".xlsx,.csv,.txt" />
<div id="fileNameLabel" class="file-info"></div>
<div class="file-picker">
<span class="file-picker-btn" aria-hidden="true">ファイルを選択</span>
<span id="fileNameLabel" class="file-picker-name" data-selected="false">選択されていません</span>
<input id="fileInput" type="file" accept=".xlsx,.csv,.txt" aria-describedby="fileNameLabel" />
</div>
<div id="encodingLabel" class="file-info"></div>
</div>

Expand Down Expand Up @@ -284,7 +287,7 @@ <h1>Matplotlib GUI</h1>
<div class="tabs" role="tablist" aria-label="表示の切り替え">
<button id="tabPlotBtn" class="tab-btn active" type="button" role="tab" aria-selected="true" aria-controls="plotPanel">プロット</button>
<button id="tabDataBtn" class="tab-btn" type="button" role="tab" aria-selected="false" aria-controls="dataPanel" tabindex="-1">データ確認</button>
<button id="tabCodeBtn" class="tab-btn" type="button" role="tab" aria-selected="false" aria-controls="codePanel" tabindex="-1">Pythonコード(beta)</button>
<button id="tabCodeBtn" class="tab-btn" type="button" role="tab" aria-selected="false" aria-controls="codePanel" tabindex="-1">Pythonコード</button>
</div>
<div id="plotPanel" class="canvas-wrap tab-panel" role="tabpanel" aria-labelledby="tabPlotBtn" aria-busy="false">
<div id="renderBusy" class="render-busy" hidden>描画中…</div>
Expand All @@ -296,17 +299,22 @@ <h1>Matplotlib GUI</h1>
<div id="codePanel" class="canvas-wrap tab-panel hidden" role="tabpanel" aria-labelledby="tabCodeBtn">
<div class="code-editor-wrap">
<div class="code-toolbar">
<div class="inline">
<input id="useCustomCode" type="checkbox" />
<label for="useCustomCode">カスタムコードを使用</label>
</div>
<button id="applyCustomCodeBtn" class="small-btn secondary" type="button">コードを反映して再描画</button>
<button id="resetCustomCodeBtn" class="small-btn ghost-btn" type="button">初期コードに戻す</button>
<span id="codeSyncStatus" class="code-sync-badge" data-state="sync" role="status">GUI と同期しています</span>
<button id="editCodeBtn" class="small-btn secondary" type="button">編集</button>
<button id="applyCustomCodeBtn" class="small-btn secondary" type="button" hidden>実行</button>
<button id="resetCustomCodeBtn" class="small-btn ghost-btn" type="button" hidden>GUI から再生成</button>
<button id="copyCodeBtn" class="small-btn ghost-btn" type="button">コピー</button>
<button id="downloadCodeBtn" class="small-btn ghost-btn" type="button">.py で保存</button>
</div>
<div class="code-editor-box">
<div id="codeLineNumbers" class="code-gutter" aria-hidden="true">1</div>
<textarea id="customPyCode" spellcheck="false" wrap="off" readonly aria-label="Pythonコード" placeholder="ファイルを読み込むと、ここに Python コードが表示されます。"></textarea>
</div>
<textarea id="customPyCode" spellcheck="false" aria-label="Pythonコード"></textarea>
<div class="help-text">
有効時はこのコードで描画します。<code>df</code>・<code>settings</code>・<code>plt</code>・<code>pd</code> が利用可能です。<code>fig</code> を必ず生成してください。Ctrl/Cmd + Enter でも再描画できます。
このコードは GUI の設定から自動で作られ、設定を変えると更新されます。「編集」を押すとコードを書き換えて実行できます(Ctrl/Cmd + Enter でも実行)。編集中は GUI の変更が図に反映されません。「GUI から再生成」で元に戻せます。「.py で保存」したファイルは、データファイルと同じフォルダに置けばローカルの Python でそのまま実行できます。
</div>
<h3 class="code-output-title">実行結果</h3>
<pre id="codeOutput" class="code-output" data-kind="ok" aria-live="polite"></pre>
</div>
</div>
</main>
Expand Down
Loading
Loading