diff --git a/AGENTS.md b/AGENTS.md index 800add7..81fa763 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,19 +1,20 @@ # matplotlib GUI 引き継ぎ資料(AI エージェント向け) 要件の正は `docs/REQUIREMENTS.md`。このファイルと食い違う場合は REQUIREMENTS.md を優先する。 -この資料は Phase 2(描画経路のコード生成化と Pythonコードタブの刷新)完了時点の実装を説明する。各 Phase の完了時に実装に合わせて更新すること(要件 A11)。 +この資料は Phase 3(出力品質: 保存 DPI・図サイズの単位・欧文フォント・プリセット・クリップボード・パネルの区分)完了時点の実装を説明する。各 Phase の完了時に実装に合わせて更新すること(要件 A11)。 ## 1. ゴールと前提 - CSV / TXT / XLSX のデータから、ブラウザ上の GUI だけで matplotlib の図を作る。 - 利用形態は GitHub Pages 配信(公開 URL: https://mioupa.github.io/matplotlib_gui/)。`file://` で直接開く使い方は非対応。 - サーバー処理を持たない。計算はすべてブラウザ内の Python(PyScript / Pyodide)で行う。GitHub Pages は静的配信のみに使う。 -- 読み込んだデータは外部に送信しない。外部への通信はライブラリとフォントの取得だけ。 +- 読み込んだデータは外部に送信しない。外部への通信はライブラリとフォントの取得だけ(フォントは日本語の Noto Sans JP(TrueType 版)、選んだときだけ欧文の Arimo / Tinos。画面用の Web フォントは fontsource の Noto Sans JP)。 - 列の指定は列番号(`__idx__N`)で行う。同名の列があっても区別できる。 - 自動読込・自動描画を保つ(「読み込む」「描画する」ボタンは置かない)。 - 図は、設定から生成した Python スクリプトの実行だけで描く(描画経路は1本)。画面の図と、書き出した `.py` の結果は一致する。 - データ確認タブは、描画から除外される先頭行(`skipRows`)をグレー(`.skipped-row`)で表示する。 -- CDN の URL はすべてバージョン固定にする。`tests/unit/test_cdn_pinning.py` が `index.html`・`js/`・`css/`・`py/` の全 URL を走査して強制する。 +- 保存 DPI(既定 300)、図サイズの単位(inch / cm / mm)、欧文フォント、スタイルプリセット、カラーパレット、クリップボードへのコピー、数式(mathtext)の案内が加わった。いずれも設定 → 生成コード → 実行の1本の経路に載る(GUI だけの特別扱いは無い)。 +- CDN の URL はすべてバージョン固定にする。`tests/unit/test_cdn_pinning.py` が `index.html`・`js/`・`css/`・`py/` の全 URL を走査して強制する。取得しない説明リンクの `matplotlib.org` は、バージョンを含むパス(`/3.10.8/...`)だけ許可する。 ## 2. ファイル構成 @@ -23,27 +24,34 @@ | `pyscript.toml` | PyScript 設定(パッケージ、`packages_cache`、`[files]` による Python ファイルの配置) | | `css/style.css` | スタイル | | `js/main.js` | エントリ。UI を設定オブジェクトに結び付ける | -| `js/defaults.js` | 設定の既定値(`DEFAULT_SETTINGS`)、色パレット(UD カラー) | +| `js/defaults.js` | 設定の既定値(`DEFAULT_SETTINGS`、`SCHEMA_VERSION` = 2)、UD カラーの色(`PALETTE`) | +| `js/units.js` | 図サイズの単位(`UNITS`、`UNIT_NAMES`、`UNIT_STEP`)と換算 `convertLength(value, from, to)`(mm を介し、to の単位で丸める: in 3 桁 / cm 2 桁 / mm 1 桁)。純粋な ES module | +| `js/palettes.js` | カラーパレット(`PALETTES`: `ud` / `tab10` / `gray`)、`getPalette`、`paletteColor(id, index)`。id は `settings.PALETTES` と一致させる。色は JS だけが持つ | +| `js/presets.js` | スタイルプリセット(`PRESETS`: standard / paper1 / paper2 / slide)、`planPreset`(適用内容を計算するだけ)、`newSeriesDefaults`(直前のプリセットを新しい系列へ)。名前は設定に入れない | | `js/code-state.js` | Pythonコードタブの状態(モード `sync` / `edit`、直近に生成したコードとその世代)。設定オブジェクトには入れない | -| `js/state.js` | 設定オブジェクトの保持・更新・購読(`getSettings`、`setPath`、`updateSeries`、`addSeries`、`removeSeries`、`subscribe`、`toJson`) | -| `js/bridge.js` | Python(`window.mplgui`)の唯一の窓口。読込・描画・保存の制御、世代管理、フォント取得の起動 | +| `js/state.js` | 設定オブジェクトの保持・更新・購読(`getSettings`、`setPath`、`updateSeries`、`addSeries`、`removeSeries`、`applyPalette`、`setLastPreset` / `getLastPreset`、`subscribe`、`toJson`)。直近に適用したプリセットはセッション中だけここに持つ | +| `js/bridge.js` | Python(`window.mplgui`)の唯一の窓口。読込・描画・保存・コピー(`makeClipboardImage`)・コードだけの更新(`script`)の制御、世代管理、日本語・欧文フォント取得の起動と待ち | | `js/startup-progress.js` | Python 起動中の段階表示。Resource Timing(`PerformanceObserver`、取得済みエントリを含む)から Pyodide 本体(`pyodide.asm.wasm`・`python_stdlib.zip`)→ パッケージ wheel(pandas / matplotlib)→ 初期化(`/py/main.py` の取得)を推定する | -| `js/font-cache.js` | 日本語フォントの取得と Cache Storage への保存 | +| `js/font-cache.js` | フォントの取得と Cache Storage への保存。種類(`japanese` / `arimo` / `tinos`)ごとの URL(`FONT_URLS`)、種類ごとに1セッション1回の取得、旧 OTF のキャッシュ削除 | | `js/ui/codeTab.js` | Pythonコードタブ(行番号、モード別のボタン、コピー、.py 保存、Ctrl/Cmd+Enter) | | `js/ui/codeOutput.js` | 「実行結果」欄(`#codeOutput`)。print の出力とトレースバックを表示する | -| `js/ui/*.js`(上記以外) | 画面部品(`colorPicker`、`dataPreview`、`fileInfo`(選択中のファイル名と文字コードの表示)、`forms`、`notify`、`plotView`(画像と `data-summary`)、`progress`、`saveFormat`、`series`、`tabs`) | -| `py/main.py` | PyScript のエントリ。JS と `mplgui` の橋渡しだけを書く(`window.mplgui` の登録、Excel 用ライブラリの遅延導入、作業フォルダ `api.SESSION.workdir` の設定) | -| `py/mplgui/settings.py` | 設定スキーマ、既定値、検証、日本語エラー(凍結 dataclass へ変換) | +| `js/ui/panelSections.js` | 設定パネルの区分(`details.panel-section`)の折りたたみ。状態は localStorage(キー `mplgui.panelSections`、`{区分の id: 開いているか}`)だけに持ち、設定オブジェクトには入れない。localStorage が使えなくても動く(すべて開く) | +| `js/ui/styleSection.js` | 体裁区分: プリセットの「適用」、カラーパレット、図サイズの単位切替(数値を換算し、ラベルと step を単位に合わせる) | +| `js/ui/saveSection.js` | 保存区分: DPI(72 / 150 / 300 / 600 のセレクトと「任意」の入力欄)。値は `save.dpi`(数値)に書く | +| `js/ui/clipboard.js` | 「クリップボードにコピー」。`` を更新する | +| `js/ui/*.js`(上記以外) | 画面部品(`colorPicker`、`dataPreview`、`fileInfo`(選択中のファイル名と文字コードの表示)、`forms`、`notify`、`plotView`(画像と `data-summary`)、`progress`、`saveFormat`(形式に応じた透過の可否と、SVG の文字・PDF の説明の出し分け)、`series`、`tabs`) | +| `py/main.py` | PyScript のエントリ。JS と `mplgui` の橋渡しだけを書く(`window.mplgui` の登録(`render` / `save` / `copyImage` / `script` / `registerFont` など)、Excel 用ライブラリの遅延導入、作業フォルダ `api.SESSION.workdir` の設定) | +| `py/mplgui/settings.py` | 設定スキーマ(version 2)、既定値、検証、日本語エラー(凍結 dataclass へ変換)、`migrate_settings`(v1 → v2) | | `py/mplgui/loader.py` | バイト列から DataFrame(文字コード判定、区切り文字、プレビュー)。読込結果に `SourceInfo`(ファイル名、種別、文字コード、区切り文字、ヘッダの有無、xlsx のシート名)を付ける。codegen が生成スクリプトの読込部に使う | | `py/mplgui/dataprep.py` | DataFrame を見て決める処理。`plan_plot` が描画計画 `PlotPlan` を作る(Y 列の自動割り当て、X の数値変換の要否、描ける点の有無、A7 の警告、データ由来の日本語 `UserError`) | | `py/mplgui/codegen.py` | 設定・`SourceInfo`・`PlotPlan` から matplotlib スクリプトを生成する(`generate_script`)。純粋な関数 | -| `py/mplgui/formats.py` | 保存形式の表(`FORMATS`)、保存解像度(`SAVE_DPI` = 120)、`build_filename`、`savefig_kwargs`。runner と codegen が共有する | -| `py/mplgui/runner.py` | スクリプトの実行(`run_script` / `run_to_image`)、Figure → 画像(PNG/JPG/SVG/PDF)、図の要約(`figure_summary`)、実行時エラーの整形 | -| `py/mplgui/fonts.py` | 日本語フォントの登録と rcParams。フォント一覧 `SANS_SERIF_PRIORITY` が唯一の定義で、生成スクリプトの rcParams もこれを使う | +| `py/mplgui/formats.py` | 保存形式の表(`FORMATS`)、保存解像度の既定値(`DEFAULT_SAVE_DPI` = 300。実際の値は `save.dpi`)、`build_filename`、`savefig_kwargs`、形式ごとの rcParams(`savefig_rc`)、PNG / JPG の画素数の上限(`MAX_RASTER_PIXELS`、`MAX_RASTER_SIDE`、`raster_pixels`、`check_raster_size`)。runner と codegen が共有する | +| `py/mplgui/runner.py` | スクリプトの実行(`run_script` / `run_to_image`)、Figure → 画像(PNG/JPG/SVG/PDF。`savefig_rc` を `rc_context` で適用、画素数の検査)、図の要約(`figure_summary`)、実行時エラーの整形、数式の誤りの判定(`is_mathtext_error`、`MathtextError`) | +| `py/mplgui/fonts.py` | フォントの登録と rcParams。日本語の一覧 `SANS_SERIF_PRIORITY`(先頭は Noto Sans JP)と、欧文フォントの表 `LATIN_FONTS`(Arimo / Tinos の表示名・候補名・mathtext フォントセット)が唯一の定義で、生成スクリプトもこれを使う。`register_font_bytes(data, kind=)` は種類(`japanese` / `arimo` / `tinos`)ごとに1回だけ登録する | | `py/mplgui/errors.py` | `UserError`(利用者が直せるエラー) | -| `py/mplgui/api.py` | JS から呼ばれる JSON 入出力の関数。読込済みデータ(`SESSION`)と、アップロードしたファイルの作業フォルダへの書き出しを持つ | +| `py/mplgui/api.py` | JS から呼ばれる JSON 入出力の関数(`render_json` / `save_json` / `copy_image_json` / `script_json` など)。読込済みデータ(`SESSION`)と、アップロードしたファイルの作業フォルダへの書き出しを持つ | | `py/mplgui/runtime.py` | 警告フィルタ(起動時) | -| `tests/unit/` | 単体テスト(CPython、Agg) | +| `tests/unit/` | 単体テスト(CPython、Agg)。`test_js_modules.py` は node で純粋な JS モジュール(units / palettes / presets / state)を検査する | | `tests/e2e/` | E2E テスト(pytest-playwright、Chromium)。`run_script_summary.py` は、生成スクリプトを CPython で実行して図の要約を出す補助(一致確認用) | | `tests/fixtures/` | 合成テストデータ(`make_fixtures.py` が生成) | | `tests/perf/bench.py` | 起動・描画の計測。結果は `docs/PERFORMANCE.md` | @@ -54,10 +62,16 @@ ## 3. 設定オブジェクト -- スキーマのバージョンは 1(`version`)。JSON のキーは camelCase。 +- スキーマのバージョンは 2(`version`)。JSON のキーは camelCase。 - 唯一の正は JS 側(`js/state.js`)の設定オブジェクト。Python は DOM を読まず、渡された JSON を `settings.parse_settings` / `parse_load_settings` で凍結 dataclass に変換して使う。 - JS の既定値(`js/defaults.js`)と Python の既定値(`settings.default_settings()`)は一致させる。`tests/unit/test_defaults_parity.py` が比較する(node が無いと skip)。 -- 主な構造: `load`(`delimiter`、`hasHeader`)、`plot`(`type`、`skipRows`、`xColumn`、`title`、`fontSize`、`figure`、`legend`、`grid`、`margins`)、`axes`(`x` / `y` / `y2`: `label`、`scale`、`min`、`max`)、`series[]`、`save`(`filename`、`format`、`transparent`)。 +- 主な構造: `load`(`delimiter`、`hasHeader`)、`plot`(`type`、`skipRows`、`xColumn`、`title`、`fontSize`、`figure`、`latinFont`、`palette`、`legend`、`grid`、`margins`)、`axes`(`x` / `y` / `y2`: `label`、`scale`、`min`、`max`)、`series[]`、`save`(`filename`、`format`、`transparent`、`dpi`、`svgText`)。 +- Phase 3 で足した項目と既定値: + - `plot.figure`: `{width: 8, height: 6, unit: "in"}`。`unit` は `in` / `cm` / `mm`。`width` / `height` は選んだ単位の値で持つ(インチに直して持たない)。Python が検証するときだけ `FigureSettings.width_in` で「値 / 1インチあたりの長さ(2.54 / 25.4)」と割ってインチにする。上限は 50 inch を選んだ単位に直した値(in 50 / cm 127 / mm 1270)で、超えると単位つきの日本語エラー。 + - `plot.latinFont`: `default` / `arimo` / `tinos`(既定 `default`)。`plot.palette`: `ud` / `tab10` / `gray`(既定 `ud`。Python は選択肢の検証だけで、色は JS の `palettes.js` が持つ)。 + - `save.dpi`: 整数(既定 300、50〜1200、整数に見える数値・数値文字列・空欄=既定値を受け付ける。それ以外は「保存 DPI」の日本語エラー)。`save.svgText`: `path`(文字を図形にする。既定)/ `text`(文字のまま)。 +- `settings.migrate_settings(raw)`: `parse_settings` / `parse_load_settings` / `parse_save_settings` が最初に通す(純粋。引数は変更しない)。version が無ければ現在のバージョン扱い。v1 → v2 は `save.dpi` が無ければ 120 にする(v1 の保存は常に 120 dpi だったので同じ出力になる)。図の単位・欧文フォント・パレット・SVG の文字は既定値で補われる。1 と 2 以外は「設定のバージョン…に対応していません」の `UserError`。 +- パレットの色、プリセットの値、単位の換算は JS だけが持つ(`js/palettes.js`、`js/presets.js`、`js/units.js`)。プリセット名は設定に入れない。「適用」は図サイズ・単位・フォントサイズ・各系列の線幅と点サイズを個別の設定に書き込むだけで、直近のプリセットは `state.js` がセッション中だけ覚える(新しい系列に、線幅と、scatter のときの点サイズを引き継ぐ)。点サイズは、プリセットが自動(標準)なら全系列を自動に戻し、数値なら scatter か点サイズを入力済みの系列だけ書き換える。パレットの切替(`state.applyPalette`)は、全系列を系列の順に新しいパレットの色で塗り直してから `plot.palette` を書く。新しい系列の既定色は現在のパレットの未使用色。 - 系列: `id`、`x`、`y`、`color`、`lineWidth`、`lineStyle`、`markerSize`、`label`、`secondaryAxis`。`markerSize: null` は「自動」(line は 0、scatter は 24)。ユーザーが数値を入れたら自動切替の対象外。 - Pythonコードタブの状態(モード `sync` / `edit`、生成したコード)は設定オブジェクトに入れず、`js/code-state.js` に持つ。スキーマは version 1 のまま。編集モードのコード本文は、`render` / `save` の別引数 `code` で渡す。「使用する」(`useCustomCode`)は廃止した。 @@ -71,14 +85,16 @@ Python が登録し、登録後に `mplgui-ready` イベントを送る。引数 | `loadFile(name, bytes, loadJson)` | ファイル名、内容、`{version, load}` | `{encoding, columns, preview, warnings}`(.xlsx は `encoding: null`) | | `render(settingsJson, code)` | 設定 JSON、`code`(null または文字列) | `code` が null: `{image, code, output, summary, seriesCount, skipRows, warnings}`。文字列: `{image, output, summary}` | | `save(settingsJson, code)` | 同上 | `{filename, mime, dataUri, output}` | +| `copyImage(settingsJson, code)` | 同上 | `{mime, dataUri, width, height, output}`(保存形式にかかわらず PNG) | +| `script(settingsJson)` | 設定 JSON | `{code}`(設定から生成した完全なスクリプトだけ。実行しない。読込済みデータが必要(無いと「先にファイルを読み込んでください。」)) | | `scriptFilename(saveFilename)` | 保存ファイル名の入力値 | 「.py で保存」のファイル名(文字列)。保存ファイル名と同じ規則(`formats.build_filename`)で拡張子を `.py` にする | -| `registerFont(bytes)` | フォントのバイト列 | `{registered}` | -| `fontStatus()` | なし | `{registered}` | +| `registerFont(bytes, kind)` | フォントのバイト列、種類(`japanese`(省略時)/ `arimo` / `tinos`。他は「未対応のフォントの種類」の `UserError`) | `{registered, kind}`(種類ごとに登録は1回だけ。2回目以降は `registered: false`) | +| `fontStatus()` | なし | `{registered, kinds}`(`registered` は日本語フォントの登録済みか、`kinds` は登録済みの種類の一覧) | `defaultCustomCode()` は廃止した(Phase 2)。 -- `code` が null(GUI 同期): 設定から生成したスクリプトの「自動描画用」(§4.1)を、読込済みの DataFrame(`df`)を渡して実行する。`image` は PNG の data URI、`code` は表示用の完全なスクリプト、`output` は実行中の print / stderr、`summary` は `runner.figure_summary` の軸ごとの要約。読込済みデータが必要。`save` は保存設定の形式・背景透過・`SAVE_DPI`(120)で書き出す(`render` のプレビューは dpi 100)。 -- `code` が文字列(編集モード): そのスクリプトを丸ごと、アップロードしたファイルのある作業フォルダ(`SESSION.workdir`)で実行する。`render` は設定を読まない(設定が不正でも、読込済みデータが無くても動く)。`save` は保存設定(`save.*`)だけを `settings.parse_save_settings` で検証して使う(描画設定が不正でも、編集したコードの図は保存できる)。GUI の設定をコードの結果に上書き適用することはしない(A8)。 +- `code` が null(GUI 同期): 設定から生成したスクリプトの「自動描画用」(§4.1)を、読込済みの DataFrame(`df`)を渡して実行する。`image` は PNG の data URI、`code` は表示用の完全なスクリプト、`output` は実行中の print / stderr、`summary` は `runner.figure_summary` の軸ごとの要約。読込済みデータが必要。`save` は保存設定の形式・背景透過・`save.dpi`・`save.svgText` で書き出す(`render` のプレビューは dpi 100、`PREVIEW_DPI`)。形式ごとの rcParams(`formats.savefig_rc`: PDF は `pdf.fonttype=42`、SVG は `svg.fonttype` = `path` / `none`)は `matplotlib.rc_context` で `savefig` の間だけ適用する(生成コードの `plt.rcParams[...]` と同じ値)。`copyImage` は保存形式にかかわらず PNG を `save.dpi` で作り、背景透過は保存設定に従う(クリップボードにコピーする画像。C5)。`script` は、保存設定(`save.*`)の変更で表示中のコードだけを作り直すのに使う(§5)。 +- `code` が文字列(編集モード。`save` / `copyImage` も同様): そのスクリプトを丸ごと、アップロードしたファイルのある作業フォルダ(`SESSION.workdir`)で実行する。`render` は設定を読まない(設定が不正でも、読込済みデータが無くても動く)。`save` は保存設定(`save.*`)だけを `settings.parse_save_settings` で検証して使う(描画設定が不正でも、編集したコードの図は保存できる)。GUI の設定をコードの結果に上書き適用することはしない(A8)。 - `loadFile` は、読み込んだファイルを元のファイル名(フォルダ部分は除く)で作業フォルダに書き出す(前のファイルは消す。失敗したら読込済みデータと書き出したファイルを破棄する)。こうして、表示中のスクリプトの `pd.read_csv("ファイル名", ...)` がそのまま動く(P5)。 - `warnings` は `{series, message}` の配列(読込時は文字列配列の場合もある。JS の `warningTexts` が吸収する)。 - 失敗: `{ok: false, error: {message, field?, detail?, traceback?, line?}, output?}`。 @@ -87,6 +103,8 @@ Python が登録し、登録後に `mplgui-ready` イベントを送る。引数 - スクリプトの実行時エラーは `runner.ScriptError`。`message` は日本語の要約(編集モードは「N行目、エラーの型: 日本語の説明」入り)、`traceback` は `plot.py` の行だけのトレースバック、`line` は最後の `plot.py` の行番号、`detail` は完全な traceback。トップレベルの `output` は、失敗するまでの print の出力(`output` 属性を持つ例外だけ)。 - GUI 同期で生成コードの実行が失敗したときは、行番号から失敗した手順(`GeneratedScript.step_for_line`)を引き、手順ごとの日本語メッセージ(`field` 付き)に置き換える(`api._step_error`)。手順に当たらなければ `GENERIC_ERROR_MESSAGE`。 - `render` は読込済みデータが無いと(GUI 同期のとき)「先にファイルを読み込んでください。」を返す。 +- 画素数の検査(`formats.check_raster_size`): PNG / JPG は、書き出す前に `int(幅inch × dpi)` × `int(高さinch × dpi)`(Agg と同じ切り捨て。`raster_pixels`)が 1 億画素(`MAX_RASTER_PIXELS`)を超える、または1辺が 65536(`MAX_RASTER_SIDE`)以上なら、`UserError`(「保存する画像が大きすぎます(幅 N × 高さ M ピクセル)。保存 DPI か図のサイズを小さくしてください。」、`field` は「保存 DPI」)にする。`runner.figure_to_bytes` で行うので、`render`(dpi 100)・`save`・`copyImage`・編集モードのどれにも効く。SVG / PDF には使わない。 +- 数式(mathtext)の書き間違い(C6): `runner.is_mathtext_error` が、matplotlib の `_mathtext.py` から出た `ValueError` または pyparsing の `ParseBaseException`(`__cause__` / `__context__` の連鎖を含む)を判定する。GUI 同期では、実行中(`tight_layout` など)でも、画像にするとき(`MathtextError`)でも、`api.MATHTEXT_ERROR`(`field` は「数式」。例 `m$^2$`、`H$_2$O`、`$\alpha$`、`\$` を案内する日本語)にする。編集モードでは、`runner.MATHTEXT_HINT` が日本語の説明(`japanese_hint`)として「N行目、ValueError: …」に付く(実行中に見つかったとき。画像にするときに見つかった場合は `field` が「数式」)。内部エラーにはしない。 ## 4.1 コード生成(Phase 2) @@ -94,14 +112,17 @@ Python が登録し、登録後に `mplgui-ready` イベントを送る。引数 - `dataprep.plan_plot(df, settings)`: `skipRows` の適用、Y 列の自動割り当て、X の数値変換の要否、描ける点があるか、数値に変換できず除外した値の警告(A7)を決める。データ由来の問題は日本語の `UserError`(列が範囲外、除外行数が多すぎる、描ける数値データが無い、など)にする。結果は凍結 dataclass の `PlotPlan` / `SeriesPlan`。 - `codegen.generate_script(settings, source, plan)` は `GeneratedScript`(`text`、`load_lines`、`output_lines`、`steps`)を返す。純粋な関数で、データには触れない。 -- スクリプトの構成: ヘッダ(使い方、`import`、日本語フォントの rcParams、`FONT_SIZE`)→ `1. データの読み込み`(`DATA_FILE`、`pd.read_csv` / `pd.read_excel`。文字コード、区切り文字、ヘッダ、シート名を明記)→ `2. 描画に使う行`(`skipRows`)→ `3. 図と軸`(`fig, ax`)→ `4. 系列` → `5. 軸(ラベル・スケール・範囲・目盛)` → `6. グリッドと凡例` → `7. 余白` → `8. 保存と表示`(`fig.savefig(...)` と `plt.show()`)。各セクションの見出しは `# ==== 番号. タイトル ====`。 +- スクリプトの構成: ヘッダ(使い方、`import`、日本語フォントの rcParams、欧文フォントの設定(下記)、`FONT_SIZE`)→ `1. データの読み込み`(`DATA_FILE`、`pd.read_csv` / `pd.read_excel`。文字コード、区切り文字、ヘッダ、シート名を明記)→ `2. 描画に使う行`(`skipRows`)→ `3. 図と軸`(`fig, ax`。単位ごとの書き方は下記)→ `4. 系列` → `5. 軸(ラベル・スケール・範囲・目盛)` → `6. グリッドと凡例` → `7. 余白` → `8. 保存と表示`(形式ごとの rcParams、`fig.savefig(...)`、`plt.show()`)。各セクションの見出しは `# ==== 番号. タイトル ====`。 +- 欧文フォント(`plot.latinFont` が `arimo` / `tinos` のときだけ。`default` ではヘッダは Phase 2 と同じ): ヘッダに `from matplotlib import font_manager` を足し、`installed = {font.name for font in font_manager.fontManager.ttflist}`、`latin = [name for name in [候補…] if name in installed][:1]`、`plt.rcParams["font.family"] = latin + ["sans-serif"]` を書く。候補は `fonts.LATIN_FONTS` の並び(Arimo: `Arimo`、`Arial`、`Liberation Sans`。Tinos: `Tinos`、`Times New Roman`、`Liberation Serif`)。入っているフォントだけを選ぶのは、無い名前を `font.family` に並べると、matplotlib が描画のたびに `findfont` の警告を出すため(ローカルで実行しても警告を出さず、どれも無ければ日本語フォントで書く)。日本語の文字は、リストの後ろの `sans-serif`(`font.sans-serif` の日本語フォント)に、matplotlib の文字単位のフォールバックで回る。Tinos(`mathtext_fontset` = `stix`)のときは `plt.rcParams["mathtext.fontset"] = "stix"` も書く(数式も Times 系の字形にする。STIX は matplotlib 同梱)。 +- `3. 図と軸`: `figsize` はインチ。`unit` が `in` なら `figsize=(幅, 高さ)`。`cm` / `mm` なら `CM_PER_INCH = 2.54`(`MM_PER_INCH = 25.4`)を定義し、`figsize=(幅 / CM_PER_INCH, 高さ / CM_PER_INCH)` と割る(コメントに単位と値を書く)。`1 / 2.54` を掛けると、20.32 cm が 7.999… inch になり、Agg の画素数(切り捨て)が 1 画素欠けるため、割り算にする(Python 側の `FigureSettings.width_in` も同じ割り算)。 +- `8. 保存と表示`: `formats.savefig_rc(format, svg_text)` が返す rcParams を、`plt.rcParams["pdf.fonttype"] = 42` / `plt.rcParams["svg.fonttype"] = "path"|"none"` としてコメント付きで `fig.savefig(...)` の前に書く(runner が `rc_context` で適用する値と同じ)。`savefig` の `dpi` は PNG / JPG のときだけ入れ、値は `save.dpi`(コメントに「N dpi」)。PDF のときは、手元の日本語フォントが OpenType(CFF)形式(macOS のヒラギノなど)だと文字が正しく表示されないことがあるという注意を、コメント2行で添える(原因は §6)。 - 変数名は、`df`(データ)、`fig` / `ax` / `ax2`(第2Y軸)、系列ごとの `x` / `y` / `ok`(欠けた行を除くマスク)、棒グラフ(複数系列)の `bar_data` / `positions` / `x_labels` / `width`。列は常に `df.iloc[:, N]`(列番号)で参照し、GUI で選んだ列名と番号はコメントに書く(同名の列を区別できる)。列名はコードに入れない。 - 自動描画用の変種(`GeneratedScript.auto_render_text`): 「データの読み込み」と「保存と表示」の行を空行にしたもの。行番号は表示中のスクリプトと同じなので、トレースバックの行番号がそのまま使える。GUI の自動描画は、これに読込済みの `df` を渡して実行する(ファイルを読み直さない)。 - 手順(`Step`): 失敗しうる行の範囲と日本語メッセージ(`field` 付きのものもある)の対応表。系列の描画(`GENERIC_ERROR_MESSAGE`)、軸のスケール・範囲(X / Y / 第2Y)、`tight_layout`、`subplots_adjust`(余白)を登録している。 - **安全性(必須)**: 列名・タイトル・ラベル・色・ファイル名・文字コードなどは信頼できない入力で、実行されるコードに入る。値は必ず `literal()`(str / int / 有限の float / bool / None だけを `repr` で出す。他の型は例外)を通して書き出し、コメントに入れる文字列は必ず `comment_text()`(改行・制御文字を空白にする)を通す。f 文字列などで、これ以外の方法で信頼できない文字列をコードに埋め込まないこと。`tests/unit/test_codegen.py` の注入テストが検査する。 - GUI の設定を、ユーザーが編集したコードの結果に上書き適用しない(A8)。編集モードのコードは、設定を使わずそのまま実行する。 -- 機能追加(Phase 3〜5)の手順: 描画の種類は `SERIES_EMITTERS`(プロット種別 → 系列の出力関数)に追加する。出力関数は `_Builder` に行を足し、失敗しうる行は `with b.step(メッセージ, field)` で包む。軸・グリッド・余白などは対応する `_emit_*` 関数に足す。データを見ないと決められないこと(列の割り当て、変換の要否、警告、データ由来のエラー)は `dataprep` の `PlotPlan` / `SeriesPlan` に項目を足して渡す。設定項目を足すときは JS の `defaults.js` と `settings.py` の両方に足す。生成するコードは pandas / matplotlib / numpy だけを使う(ローカルの Python でそのまま動かすため)。新しい行を足したら、単体テストで「全体を実行した図」と「自動描画用の図」が一致することも確かめる。 -- 生成スクリプトの保存ファイル名(`savefig` の引数)は `formats.build_filename` で決める。保存形式と解像度は `formats.savefig_kwargs` / `SAVE_DPI` で、GUI の「保存」と同じにする。 +- 機能追加(Phase 3〜5)の手順: 描画の種類は `SERIES_EMITTERS`(プロット種別 → 系列の出力関数)に追加する。出力関数は `_Builder` に行を足し、失敗しうる行は `with b.step(メッセージ, field)` で包む。軸・グリッド・余白などは対応する `_emit_*` 関数に足す。データを見ないと決められないこと(列の割り当て、変換の要否、警告、データ由来のエラー)は `dataprep` の `PlotPlan` / `SeriesPlan` に項目を足して渡す。設定項目を足すときは JS の `defaults.js` と `settings.py` の両方に足す。生成するコードは pandas / matplotlib / numpy だけを使う(ローカルの Python でそのまま動かすため)。新しい行を足したら、単体テストで「全体を実行した図」と「自動描画用の図」が一致することも確かめる。保存形式ごとの rcParams は `formats.savefig_rc` に足す(runner と codegen が同じ表を使う)。欧文フォントを足すときは `fonts.LATIN_FONTS`・`FONT_FILENAMES`、JS の `font-cache.js` の `FONT_URLS`・`bridge.js` の `LATIN_NAMES`、`settings.LATIN_FONTS`、`index.html` の選択肢を揃える。 +- 生成スクリプトの保存ファイル名(`savefig` の引数)は `formats.build_filename` で決める。保存形式・解像度・形式ごとの rcParams は `formats.savefig_kwargs`(解像度は `save.dpi`)/ `formats.savefig_rc` で、GUI の「保存」と同じにする。 ### runner(スクリプトの実行) @@ -114,8 +135,9 @@ Python が登録し、登録後に `mplgui-ready` イベントを送る。引数 ## 5. 描画パイプライン -- 描画は設定変更の 250 ms 後(デバウンス)。読込は、ファイル選択・ヘッダ変更で 0 ms、区切り文字の変更で 450 ms 後。保存設定(`save.*`)の変更では再描画しない。`skipRows` の変更は Python を呼ばず、データ確認表のグレー表示だけ即時に更新する。 +- 描画は設定変更の 250 ms 後(デバウンス)。読込は、ファイル選択・ヘッダ変更で 0 ms、区切り文字の変更で 450 ms 後。保存設定(`save.*`)の変更では再描画しない(画像の画素は保存設定に依存しないため)。`skipRows` の変更は Python を呼ばず、データ確認表のグレー表示だけ即時に更新する。 - 起動中の `#progress`(`aria-live="polite"`)は段階表示: 「Python 実行環境を読み込み中…」→「ライブラリを読み込み中…(pandas ✓, matplotlib …)」→「ライブラリを初期化中…」→ 準備完了で消える(フォント取得の表示が続く場合あり)。`` の `data-startup-stage`(`runtime` / `packages` / `init` / `ready`)に現在の段階が入る。新しい id は無い。読込失敗の案内(`data-load-failed`)は上書きしない。 +- 保存設定(`save.*`: ファイル名・形式・背景透過・DPI・SVG の文字)の変更は、GUI 同期のときだけ、250 ms 後に `script` API でコードだけを作り直す(`bridge.js` の `refreshScript`)。画像は描き直さず、コードの世代(`#customPyCode` の `data-generation`)は表示中の画像のまま(`code-state.getGeneratedGeneration()`)。描画が予約中・実行中のときは何もしない(描画が新しいコードを返す。実行中だったら終了後にもう一度確かめる)。コードの更新に失敗したら(保存設定の不正など)エラーを表示し、次に成功したときに直近の描画成功のステータスへ戻す。編集モードでは何もしない。 - 描画要求には世代番号を付ける。Python の描画は同時に最大1つ。実行中に新しい要求が来たら「やり直し」の印だけ付け、終了後に最新の設定で1回だけ描く(合流)。古い世代の結果は画像にもステータスにも反映しない。 - Python の起動前に選んだファイルや変えた設定は保持し、起動後に最新の内容で1回読み込んで描く(起動キュー)。 - Pythonコードタブのモード(`js/code-state.js`): @@ -123,23 +145,30 @@ Python が登録し、登録後に `mplgui-ready` イベントを送る。引数 - `edit`: 利用者が書き換えたコードを「実行」(Ctrl/Cmd+Enter)したときだけ描く。GUI の変更でも、ファイルの読込でも自動では描かず、コードも更新しない(ステータスに warning で「編集中のため反映されません」と出す)。読込は行い、ファイルは作業フォルダに置かれる。フォント取得後の描き直しも、実行したことがあるときだけ行う。 - 「編集」で `edit`、「GUI から再生成」で `sync` に切り替える。切り替えのたびに、予約中・実行中の描画を無効にする(世代を進め、結果は画像にもコードにもステータスにも反映しない)。このため `data-render-generation` は、モード切替の直後に、表示中の画像の世代より先に進んだままになることがある。E2E は `tests/e2e/helpers.py` の専用ヘルパー(`wait_code_generated`、`wait_image_generation_changed`、`run_edited_code`、`wait_run_finished`)で待つこと。 - `sync` の描画は読込済みデータが必要。`edit` は Python が動いていればよい(コードが自分でファイルを読む)。 -- E2E が使う状態は `` の data 属性: `data-app-state`(`starting` / `ready` / `failed`)、`data-data-state`(`none` / `loading` / `ready` / `error`)、`data-render-state`(`idle` / `pending` / `rendering`)、`data-render-generation`、`data-font-state`(`idle` / `loading` / `ready` / `failed`)、`data-load-count`。表示中の画像は `#plotArea img` の `data-generation` と `data-summary`(`figure_summary` の JSON)を持つ。ステータスは `#status` の `data-kind`。Pythonコードタブ用に、`` の `data-code-mode`(`sync` / `edit`)、`#customPyCode`(textarea)の `data-generation`(表示中のコードを生成した世代。GUI 同期のとき、同じ世代の画像と対応する)、`#codeOutput` の `data-kind`(`ok` / `error`)、`#codeSyncStatus` の `data-state`(`sync` / `edit`)がある。 +- 欧文フォント(`plot.latinFont`): 選んだときだけ取得する(既定の `default` では取得しない)。Python の起動前に選んでいたら、起動後に取得する。取得中の描画・保存・コピーは、`waitForFonts` が最大 `FONT_FIRST_RENDER_WAIT_MS`(8 秒)待つ。超えたらその種類は待ちを打ち切り、標準フォントで描き、取得・登録が済んだら(選択中のままなら)現在の設定で1回だけ描き直す(編集モードでは「実行」したことがあるときだけ)。取得は種類ごとにセッション中1回で、失敗しても再試行しない。失敗したら、選択中のあいだ警告「欧文フォント Arimo(または Tinos)を取得できなかったため、標準のフォントで描画しています。」を常時警告に1度だけ出し、標準(`default`)に戻すと消える(取得を再試行しない)。編集モードでも欧文フォントの取得・登録はするが、描画もコードも触らない。取得中は進捗バナーに「欧文フォントを取得中…」。 +- 左パネルは `details.panel-section`(`#sectionLoad` データ読み込み、`#sectionSeries` 系列、`#sectionAxes` 軸・目盛、`#sectionStyle` 体裁、`#sectionSave` 保存)に分かれ、既定はすべて開く。折りたたみの状態は localStorage の `mplgui.panelSections` に持つ(`panelSections.js`)。ステータス(`#status`)は区分の外にあり、閉じていても見える。「注釈・解析」の区分は Phase 5 まで作らない。体裁区分は、プリセット(`#stylePreset`、「適用」`#applyPresetBtn`)、`#colorPalette`、`#latinFont`、`#fontSize`、図サイズ(`#figWidth` / `#figHeight` / `#figUnit`)、凡例位置、グリッド、余白。軸・目盛区分には数式の案内(`#mathtextHelp`。matplotlib 3.10.8 の mathtext ページへのリンク)がある。保存区分は `#saveDpi`(+ 任意の `#saveDpiCustom`)、SVG のときだけ `#svgTextGroup`(`#svgText`)、PDF のときだけ `#pdfHelp`、`#savePlotBtn`、`#copyPlotBtn`。 +- 図サイズの単位を替えると、入力欄の数値を換算し(`convertLength`。空欄・不正な入力はそのまま)、図の大きさは変えない。ラベル(`図幅(inch)` など)と step は単位に合わせる。プリセットの「適用」は値を書き込むだけで、描画は通常の変更どおり。パレットを替えると、色の選択肢と各系列の色を描き直す。 +- 「クリップボードにコピー」(`clipboard.js`): 保存 DPI の PNG を `copyImage` で作り、`ClipboardItem` に `Promise` として渡して `navigator.clipboard.write` する(Safari がクリックの中での呼び出しを要求するため、クリックの処理では何も `await` せずに `write` を呼び、画像は渡した Promise の中で作る)。対応は `window.isSecureContext`(https または localhost)、`navigator.clipboard.write`、`ClipboardItem`(`ClipboardItem.supports("image/png")` があれば true)。編集モードでは編集中のコードの図をコピーする。 +- E2E が使う状態は `` の data 属性: `data-app-state`(`starting` / `ready` / `failed`)、`data-data-state`(`none` / `loading` / `ready` / `error`)、`data-render-state`(`idle` / `pending` / `rendering`)、`data-render-generation`、`data-font-state`(日本語フォント。`idle` / `loading` / `ready` / `failed`)、`data-latin-font-state`(選択中の欧文フォント。`idle`(標準を選択中)/ `loading` / `ready` / `failed`)、`data-copy-state`(`idle` / `copying` / `done` / `failed` / `unsupported`)、`data-load-count`。表示中の画像は `#plotArea img` の `data-generation` と `data-summary`(`figure_summary` の JSON)を持つ。ステータスは `#status` の `data-kind`。Pythonコードタブ用に、`` の `data-code-mode`(`sync` / `edit`)、`#customPyCode`(textarea)の `data-generation`(表示中のコードを生成した世代。GUI 同期のとき、同じ世代の画像と対応する)、`#codeOutput` の `data-kind`(`ok` / `error`)、`#codeSyncStatus` の `data-state`(`sync` / `edit`)がある。 ## 6. 起動とフォント - PyScript 2026.7.3(Pyodide 314.0.3 / Python 3.14)。`pyscript.toml` は `packages = ["pandas", "matplotlib"]`、`packages_cache = "passthrough"`(micropip を使わない)。 - openpyxl と et_xmlfile は Pyodide 同梱ではないので、初めて .xlsx を読むときに `py/main.py` が、バージョン固定の wheel URL(`files.pythonhosted.org`)を `pyodide_js.loadPackage` に渡して導入する。起動時には導入しない。 - matplotlib のバックエンドは Agg(`py/main.py` で `matplotlib.use("Agg")`。pyplot を import する前に指定する)。 -- 日本語フォントは固定 URL の jsDelivr(`noto-cjk@Sans2.004` の `NotoSansCJKjp-Regular.otf`、約16 MB)から取得し、Cache Storage(キャッシュ名 `mplgui-fonts-v1`)に保存する。2回目以降の訪問ではダウンロードしない。取得はセッション中1回で、失敗しても再試行せず、警告を1度だけ出してフォールバックフォントで続行する。 -- フォント取得は、進みが 20 秒止まる(ヘッダが届かない、または本文が進まない)と失敗扱いにする(`FONT_STALL_MS`)。 -- 最初の描画はフォント取得を最大 8 秒待つ(`FONT_FIRST_RENDER_WAIT_MS`)。超えたらフォールバックフォントで描き、取得が終わったら現在の設定で1回だけ描き直す。 +- 日本語フォントは、固定 URL の jsDelivr(`@expo-google-fonts/noto-sans-jp@0.4.4/400Regular/NotoSansJP_400Regular.ttf`、約5.7 MB。TrueType 版 Noto Sans JP)から取得し、Cache Storage(キャッシュ名 `mplgui-fonts-v1`)に保存する。2回目以降の訪問ではダウンロードしない。取得はセッション中1回で、失敗しても再試行せず、警告を1度だけ出してフォールバックフォントで続行する。 +- Noto Sans CJK JP の OTF(CFF 形式、約16 MB)から替えた理由: matplotlib 3.10 は CFF 形式のフォントを `pdf.fonttype=42` で正しく埋め込めない(TrueType 用の FontFile2 に CFF を入れ、PDF が壊れる)。TrueType 版は同じデザイン・同じ文字幅。ハングルと中国語専用の漢字は含まれない(JIS 第1〜第4水準は含む)。旧 OTF のキャッシュ(URL `noto-cjk@Sans2.004/...NotoSansCJKjp-Regular.otf`)が `mplgui-fonts-v1` に残っていたら、日本語フォントの取得時に1回だけ削除する(失敗は無視する)。matplotlib に登録するフォント名は `Noto Sans JP`(`SANS_SERIF_PRIORITY` の先頭)。 +- 欧文フォント(選んだときだけ取得。起動時には取得しない): Arimo は `@expo-google-fonts/arimo@0.4.3/400Regular/Arimo_400Regular.ttf`、Tinos は `@expo-google-fonts/tinos@0.4.2/400Regular/Tinos_400Regular.ttf`(各 約0.3〜0.5 MB。jsDelivr、バージョン固定)。同じ Cache Storage に保存し、種類ごとにセッション中1回だけ取得する。ライセンスは3つとも SIL OFL 1.1(Noto Sans JP のライセンス全文: `https://cdn.jsdelivr.net/npm/@expo-google-fonts/noto-sans-jp@0.4.4/LICENSE_FONT`。Arimo は `@expo-google-fonts/arimo@0.4.3/LICENSE_FONT`、Tinos は `@expo-google-fonts/tinos@0.4.2/LICENSE_FONT`)。リポジトリにはフォントを置かない(実行時に取得するだけ)。登録は `registerFont(bytes, kind)`。画面用の Web フォントは `@fontsource/noto-sans-jp@5.3.0`(`index.html`)。 +- フォント取得は、進みが 20 秒止まる(ヘッダが届かない、または本文が進まない)と失敗扱いにする(`FONT_STALL_MS`。日本語・欧文とも)。 +- 日本語フォントは、最初の描画だけ取得を最大 8 秒待つ(`FONT_FIRST_RENDER_WAIT_MS`)。欧文フォントは、選んだフォントごとに1回だけ待つ。超えたらフォールバックフォントで描き、取得が終わったら現在の設定で1回だけ描き直す(§5)。保存とコピーも、取得中なら同じ時間まで待つ。 - テスト専用に、`window.__MPLGUI_TEST_OVERRIDES__ = {fontStallMs, fontFirstRenderWaitMs}` で上の時間を短縮できる。 ## 7. 文字コード判定・数値変換警告・ステータス - 文字コード(`loader.detect_encoding`): BOM 付きなら utf-8-sig → UTF-8 → CP932 と EUC-JP。両方でデコードできるときは、半角カナ・私用領域・制御文字などの「文字化けらしい文字」が少ないほうを採用し、同数なら CP932。どれでも読めなければ日本語のエラーにする。判定結果は `#encodingLabel` に表示する。 - 数値に変換できず除外した値は、系列ごとに警告にする(`dataprep.dropped_warning`。`dataprep.plan_plot` が集める)。 -- ステータス(`#status`)の種別は `ok` / `warning` / `error`。警告は成功メッセージに添えて表示し、`error` のときは `data-kind="error"`。日本語フォント取得失敗のような常時有効な警告は、以後の表示にも添える。 +- ステータス(`#status`)の種別は `ok` / `warning` / `error`。警告は成功メッセージに添えて表示し、`error` のときは `data-kind="error"`。日本語フォント取得失敗のような常時有効な警告は、以後の表示にも添える。欧文フォントの取得失敗の警告は、そのフォントを選んでいるあいだだけ常時警告に出し(1度だけ)、選び直すと消える(`removeStickyWarning`)。 +- コピーのメッセージ: 成功は `ok`「図をクリップボードにコピーしました(PNG、{dpi} dpi、{幅} × {高さ} ピクセル)。」、非対応のブラウザは `warning`「このブラウザでは図をクリップボードにコピーできません。「保存する」で PNG を保存してください。」、ブラウザが書き込みを拒否したときは `error`「クリップボードにコピーできませんでした。ページをクリックしてから、もう一度試してください。」。Python 側の失敗(データ無し、保存設定の不正、画素数、数式など)は保存と同じ表示で、上の拒否のメッセージは重ねない。 ## 8. 警告フィルタ @@ -170,6 +199,17 @@ uv run pytest tests/e2e # E2E(Chromium とローカルの静的サー - `test_fonts.py`: フォント一覧が生成スクリプトと rcParams で共通であること。`test_api.py`: 新しい `render` / `save` / `loadFile`(作業フォルダへの書き出し)/ エラーの形。 - E2E(`tests/e2e/`)のうち Phase 2 で加えたもの: `test_code_tab.py`(同期表示、GUI 変更への追従、コピー、.py 保存、編集・実行、編集中の GUI 変更が反映されないこと、「GUI から再生成」、Ctrl/Cmd+Enter、トレースバックと print の表示、編集モードでの保存、狭い画面)、`test_codegen_consistency.py`(下記)。 - 生成コードの一致確認(要件 7 章): `test_codegen_consistency.py` が、ブラウザで設定した図(line / scatter / bar)の `.py` を実際にダウンロードし、`tests/fixtures` のデータと同じフォルダに置いて CPython で実行する(`run_script_summary.py` が `MPLBACKEND=Agg`、`PYTHONPATH=py` で実行して `figure_summary` を出力する)。その要約を、ブラウザの画像の `data-summary` と比べる(系列数、軸範囲、ラベル、凡例、スケール、棒グラフの目盛ラベル)。フォントは環境で違うので、画素やレイアウトは比べない。 +- Phase 3 で加えた単体テスト: + - `test_settings.py`: 新しい既定値、単位ごとの図サイズ(インチへの換算と上限)、新しい項目の検証と日本語エラー、DPI の受け入れ(整数・整数に見える数値・数値文字列・空欄)、`migrate_settings`(v1 → v2 で dpi 120、既存の dpi を保つ、`save` が無い設定、3つのパーサ経由、v2 はそのまま、未対応バージョンはエラー)。 + - `test_codegen.py`: 単位ごとの `figsize` の実測値と、cm / mm の丸い値で画素数が欠けないこと、図サイズ部分の文面、`savefig` の `dpi` が PNG / JPG のときだけであること、PDF / SVG の rcParams の行(`formats` と同じ値)、PDF で TrueType が埋め込まれること、欧文フォントのヘッダ(`default` は従来どおり。入っていないフォントで `findfont` の警告が出ないこと、入っていれば使うこと)。 + - `test_runner.py`: PDF が Type 42 であること(スクリプトの助けなしで)、SVG の文字の切替、DPI で画素数が変わること、画素数の上限(ベクター形式には効かない)、`is_mathtext_error`、変換時の数式エラーが `field` 付きの `UserError` になること、`raster_pixels` が Agg と同じ切り捨てであること。 + - `test_api.py`: `save` の DPI と SVG の文字、v1 設定は 120 dpi、`copyImage`(PNG、保存 DPI、背景透過、編集モード、エラーの形)、PDF が TrueType 埋め込みで出力に font の行が混じらないこと、画素数の上限エラー、`registerFont` の種類と `fontStatus`、`script_json`(実行せずコードを返す、エラー)、数式エラー(GUI 同期の `render` / `save` / `copyImage`、軸ラベル・凡例名、編集モードの2経路)。 + - `test_fonts.py`(日本語の先頭が Noto Sans JP)、`test_js_modules.py`(node。単位換算が図の大きさを保つこと、プリセットの点サイズの規則、パレットと state、プリセット無しの `addSeries` が従来どおり)、`test_cdn_pinning.py`(`matplotlib.org` はバージョン付きのパスだけ許可)。 +- Phase 3 で加えた E2E: + - `test_panel_sections.py`(区分の順序・名前・既定で開く、閉じると中身が隠れる、閉じてもステータスが見える、リロードで状態が残る、localStorage が例外を投げても動く、折りたたみ後も描画できる)、`test_style_section.py`(単位切替で数値が換算され図の大きさが変わらない、空欄はそのまま、プリセットは「適用」でだけ効く、scatter の点サイズ、パレットで全系列と色選択パネルが塗り直される)、`test_save_section.py`(DPI の画素数とエラー、保存設定の変更にコードが追従して再描画しない、SVG の文字の切替と PDF の説明、PDF に日本語の TrueType が埋め込まれる)、`test_latin_font.py`(選んだときだけ取得、失敗の警告は1回で再試行しない、リロード後は Cache Storage から、遅いときはフォールバックで描いて1回だけ描き直す、編集モードでは取得するが描かない)、`test_clipboard.py`(保存 DPI の PNG、編集モード、データ無しのエラー、非対応のブラウザの警告)、`test_mathtext_help.py`(数式の案内とリンク、数式エラーの日本語メッセージと復帰)。`test_codegen_consistency.py` は、cm の図サイズなど Phase 3 の設定も一致確認の対象にした。 + - 実フォントを jsDelivr から取得するテストがある(`test_latin_font.py` の一部、`test_save_section.py` の PDF 埋め込みなど。インターネット接続が必要)。`test_font.py`(日本語フォントの失敗・停止・遅延・キャッシュ)は、matplotlib 同梱の小さな TTF を `page.route` で代役にして、5.7 MB の取得を避ける。 + - クリップボードのテストは `page.context.grant_permissions(["clipboard-read", "clipboard-write"])` を許可してから、`navigator.clipboard.read()` で画像を読み戻す。 + - `helpers.py` の Phase 3 用ヘルパー: `natural_size`、`act_and_wait_render`、`series_color`、`palette_grid_colors`、`ARIMO_GLOB` / `TINOS_GLOB`、`image_size`、`save_bytes`、`set_save_dpi`、`latin_font_state` / `wait_latin_state`、`copy_state` / `wait_copy_state`、`read_clipboard_png_size`、`wait_code_contains`。 - E2E は要素を id だけで選ぶ。`index.html` の id は安定させ、変えるときはテストも直す。アプリ固有の待機・操作は `tests/e2e/helpers.py` にまとめる。 - テストデータは `tests/fixtures/` の合成データだけ(公開リポジトリなので実データを置かない)。 - テスト用サーバー(`tests/e2e/server.py`)は listen backlog を 128 に広げている。既定の 5 だと、Chromium が ES モジュールを一斉に取得したときに接続が切れて、アプリが起動しないことがある。 @@ -179,6 +219,7 @@ uv run pytest tests/e2e # E2E(Chromium とローカルの静的サー - CI(`.github/workflows/tests.yml`): push で単体テスト、プルリクエストで E2E。E2E は Jekyll でビルドした `_site` を `/matplotlib_gui/` 配下で配信して検証する。 - Jekyll は `_` で始まるファイルを既定で配信しない。`__init__.py` は `_config.yml` の `include` で明示している。開発用ファイル(`docs/`、`tests/`、`AGENTS.md`、`pyproject.toml`、`uv.lock`、`.github/` など)は `exclude`。 +- `test_cdn_pinning.py` は、フォント(`@expo-google-fonts/*@x.y.z`、`@fontsource/*@x.y.z`)の jsDelivr URL と、バージョン付きのパスの `matplotlib.org`(取得しない説明リンク)を許可する。新しい JS モジュール(`js/units.js` など)は静的ファイルとしてそのまま公開される(`pyscript.toml` は Python ファイルだけ)。 - 新しい Python モジュールを追加したら、`pyscript.toml` の `[files]` に必ず追加する(`tests/unit/test_pages_publish.py` が参照ファイルの公開を確認する)。 - matplotlib 3.10 では `tight_layout()` 後に `PlaceHolderLayoutEngine` が残り、`savefig` が余分に全体を再描画する。`runner.drop_placeholder_layout_engine` がこれを外している(出力は同一)。 @@ -188,7 +229,9 @@ uv run pytest tests/e2e # E2E(Chromium とローカルの静的サー - push やプルリクエストの作成は、オーナーの確認を取ってから行う。 - ローカル環境のパスを、リポジトリ内の文書やコードに書かない。 -## 13. Phase 2 でオーナーが決めたこと(2026-10-08) +## 13. オーナーが決めたこと + +### 13.1 Phase 2(2026-10-08) - 旧「Pythonコード(beta)」タブの名前空間(`get_plot_data`、従来形式の settings dict、`plotted_count`)は、互換を残さずに廃止した。 - 生成スクリプトの最後は `fig.savefig(...)`(GUI の「保存」と同じ形式・解像度)と `plt.show()`。 @@ -196,3 +239,17 @@ uv run pytest tests/e2e # E2E(Chromium とローカルの静的サー - 棒グラフ(1系列・複数系列)は、カテゴリの目盛ラベルを保つ(Phase 1 の不具合を修正)。複数系列は、棒の位置にラベルを付ける(25個を超えたら間引く)。 - line / scatter で X が数値でない列(文字列・日時)のとき、Phase 1 と同じ見た目を保つため、生成コードに `ax.set_xscale("linear")` を入れる(目盛は位置の番号になる)。D5(日付軸)で見直す(Phase 4)。 - ファイル選択欄に、選んだファイル名を欄の中に表示する(`#fileNameLabel`。`input` は選択のたびに空に戻すため、JS が表示する)。 + +### 13.2 Phase 3(2026-10-08) + +- 保存 DPI の既定値は 300(U1)。選択肢は 72 / 150 / 300 / 600 と任意の値。プレビューは今までどおり dpi 100 で描く。 +- 図サイズの既定の単位は inch(8×6 を維持。U2)。cm と mm も選べる。単位を切り替えたら入力欄の数値を換算する(図の大きさは変えない。例: 8 inch → 20.32 cm)。設定には「選んだ単位の値」と単位を持つ(インチに直して持たない)。 +- 欧文フォントは Arimo(Arial 互換)と Tinos(Times 互換。U3)。ライセンスはどちらも SIL OFL 1.1(要件定義書の当初の「Apache 2.0」は誤り)。選んだときだけ、バージョン固定の jsDelivr から取得する。日本語の文字は Noto Sans JP で描く(matplotlib の文字単位のフォールバック)。Tinos のときは数式(mathtext)を STIX にする。 +- 日本語フォントを、Noto Sans CJK JP(OTF、CFF 形式)から、同じデザインの TrueType 版 Noto Sans JP に替えた。matplotlib 3.10 は CFF 形式のフォントを `pdf.fonttype=42` で正しく埋め込めない(TrueType 用の FontFile2 に CFF を入れ、PDF が壊れる)ため。 +- PDF は Type 42(`pdf.fonttype=42`)で書き出す。SVG の文字は「パスに変換」(既定)と「テキストのまま」を選べる。 +- スタイルプリセット(標準 / 論文1段組 / 論文2段組 / スライド16:9)は「適用」ボタンで値をまとめて書き込むだけで、設定にプリセット名を残さない。点サイズは、点を表示している系列(scatter、または点サイズを入力した line)だけ書き換える(標準は点サイズを「自動」に戻す)。適用後に追加した系列は、直前に適用したプリセットの値(線幅と、scatter のときの点サイズ)を使う(セッション中だけ JS が覚える)。 +- カラーパレット(UD カラー / tab10 / グレースケール)を切り替えたら、すべての系列を、系列の順に新しいパレットの色で塗り直す(個別に選んだ色も上書きする)。 +- クリップボードにコピーする画像(C5)は、保存 DPI と同じ解像度の PNG。背景透過は保存設定に従う。 +- 左パネルを「データ読み込み」「系列」「軸・目盛」「体裁」「保存」の区分に整理し、各区分を `
` で折りたためるようにする。折りたたみの状態は localStorage に保持する。「注釈・解析」の区分は Phase 5 まで作らない。 +- 設定スキーマを version 2 にした。v1 からの移行では、v1 の保存が常に 120 dpi だったため `save.dpi` を 120 にする。 +- 「描画設定: 先頭からスキップする行数」の欄名を「描画から除外する先頭行数」に改めた(「描画設定」の区分が無くなったため)。 diff --git a/README.md b/README.md index 5a080ed..37ac44e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,10 @@ https://mioupa.github.io/matplotlib_gui/ を開いてください。`index.html` 1. 「入力ファイル」で `.csv` / `.txt` / `.xlsx` を選びます。 2. 読み込みと描画は自動で行われます(「描画する」ボタンはありません)。設定を変えると、すぐ再描画されます。 3. 「描画系列」でX列・Y列・色などを選び、必要なら系列を追加します。 -4. 「保存設定」でファイル名と形式を選び、「保存する」で書き出します。 +4. 「体裁」でフォント・図のサイズ・プリセットなどを整えます。 +5. 「保存」でファイル名・形式・DPI を選び、「保存する」で書き出すか、「クリップボードにコピー」で他のアプリに貼り付けます。 + +左の設定パネルは「データ読み込み」「系列」「軸・目盛」「体裁」「保存」の区分に分かれていて、見出しを押すと折りたためます(折りたたみの状態は、このブラウザに覚えておきます)。 対応する文字コードは UTF-8、UTF-8(BOM付き)、Shift_JIS(CP932)、EUC-JP です。判定した文字コードは、読み込み後に画面へ表示されます。 @@ -18,7 +21,14 @@ https://mioupa.github.io/matplotlib_gui/ を開いてください。`index.html` - 系列カード(複数系列、X/Y列、色、線幅、線種、点サイズ、凡例名)と、第2Y軸 - プロット種別: line / scatter / bar - 軸の対数表示と範囲指定、余白指定、主目盛線・副目盛線、凡例位置 -- 保存: png / jpg / svg / pdf、背景透過 +- 保存: png / jpg / svg / pdf、背景透過。PNG・JPG は保存 DPI(72 / 150 / 300 / 600 または任意の値。既定は 300)を指定できます。プレビューは 100 dpi で表示します +- PDF は文字をフォントとして埋め込みます(Type 42)。Illustrator などで文字を編集できます。SVG は、文字を「パスに変換」(既定。どの環境でも同じ見た目)か「テキストのまま」(編集できる。開く環境に同じフォントが無いと見た目が変わります)から選べます +- 図のサイズ: inch / cm / mm で指定(単位を切り替えると、図の大きさを変えずに数値を換算します) +- スタイルプリセット(標準 / 論文1段組 / 論文2段組 / スライド16:9): 「適用」で、図のサイズ・フォントサイズ・線幅・点サイズをまとめて設定します +- カラーパレット(UD カラー / tab10 / グレースケール): 切り替えると、すべての系列の色を、系列の順に新しいパレットの色へ塗り直します +- 欧文フォント: 標準(Noto Sans JP)、Arimo(Arial 互換)、Tinos(Times 互換)。英数字と記号だけを選んだフォントで書き、日本語は Noto Sans JP で書きます(Tinos のときは数式も Times 系の字形になります) +- クリップボードへのコピー: プレビュー中の図を、保存 DPI の PNG として画像でコピーします(背景透過は保存設定に従います)。対応ブラウザは Chrome / Edge 98 以降、Safari 13.1 以降、Firefox 127 以降で、https で開いている必要があります。非対応のときは「保存する」で PNG を保存してください +- 数式・上付き・下付き: タイトル・軸ラベル・凡例名で、`$` で囲んだ部分が数式になります。例: `m$^2$`(上付き)、`H$_2$O`(下付き)、`$\alpha$`(ギリシャ文字)。`$` を文字として書くときは `\$` とします。書き方を間違えると、日本語のエラーが表示されます - データ確認タブ: 先頭100行を表示し、描画から除外される先頭行をグレーで表示 - Pythonコードタブ: 設定から生成した matplotlib のスクリプトを表示(下記) - 数値に変換できず除外した値の警告、日本語のエラーメッセージ @@ -38,7 +48,7 @@ https://mioupa.github.io/matplotlib_gui/ を開いてください。`index.html` 2. `pip install pandas matplotlib numpy` を実行します。`.xlsx` を読むときは `openpyxl` も入れます。 3. `python plot.py` で実行します(ファイル名は書き出した名前に読み替えてください)。 -日本語フォントは、見つかったものを使います。無い場合も実行はできますが、日本語は豆腐(□)で表示されます。 +日本語フォントは、見つかったものを使います。無い場合も実行はできますが、日本語は豆腐(□)で表示されます。欧文フォントを選んだ図は、Arimo / Tinos(または Arial / Times New Roman、Liberation)が入っていればそれを使い、無ければ日本語フォントで書きます。PDF の文字は TrueType 形式のフォントなら正しく埋め込まれます。macOS のヒラギノなど OpenType(CFF)形式のフォントでは、PDF の文字が正しく表示されないことがあります。そのときは Noto Sans JP(TrueType 版)を入れてください。 ## プライバシー @@ -47,10 +57,22 @@ https://mioupa.github.io/matplotlib_gui/ を開いてください。`index.html` | ホスト | 取得するもの | |---|---| | `pyscript.net` | PyScript 本体 | -| `cdn.jsdelivr.net` | Pyodide 本体と同梱パッケージ(pandas / matplotlib など)、画面用フォント(Noto Sans JP)、グラフ用の日本語フォント(Noto Sans CJK JP) | +| `cdn.jsdelivr.net` | Pyodide 本体と同梱パッケージ(pandas / matplotlib など)、画面用フォント(Noto Sans JP)、グラフ用のフォント(日本語の Noto Sans JP、選んだときだけ Arimo / Tinos) | | `files.pythonhosted.org` | openpyxl の wheel(`.xlsx` を初めて開くときだけ) | -初回の訪問では、Pyodide とライブラリ(数十 MB)と、日本語フォント(約 16 MB)をダウンロードするため、時間がかかります。2回目以降はキャッシュを使います(フォントはブラウザの Cache Storage に保存し、再ダウンロードしません)。 +初回の訪問では、Pyodide とライブラリ(数十 MB)と、日本語フォント(約 5.7 MB)をダウンロードするため、時間がかかります。欧文フォントは、選んだときだけ取得します(各 約 0.3〜0.5 MB)。2回目以降はキャッシュを使います(フォントはブラウザの Cache Storage に保存し、再ダウンロードしません)。 + +## フォントとライセンス + +グラフに使うフォントは、実行時に jsDelivr から取得します(このリポジトリには含めていません)。いずれも SIL Open Font License 1.1 です。 + +| フォント | 用途 | ライセンス全文 | +|---|---|---| +| Noto Sans JP(TrueType 版) | グラフの日本語、画面用のフォント | https://cdn.jsdelivr.net/npm/@expo-google-fonts/noto-sans-jp@0.4.4/LICENSE_FONT | +| Arimo | 欧文フォント(Arial 互換) | https://cdn.jsdelivr.net/npm/@expo-google-fonts/arimo@0.4.3/LICENSE_FONT | +| Tinos | 欧文フォント(Times 互換) | https://cdn.jsdelivr.net/npm/@expo-google-fonts/tinos@0.4.2/LICENSE_FONT | + +画面用の Web フォントは、`@fontsource/noto-sans-jp@5.3.0`(Noto Sans JP、同じく SIL OFL 1.1)を jsDelivr から読み込みます。グラフの日本語に TrueType 版を使うのは、PDF に文字をフォントとして埋め込む(Type 42)には TrueType 形式が必要なためです。 ## 開発 diff --git a/css/style.css b/css/style.css index 3844322..c55598e 100644 --- a/css/style.css +++ b/css/style.css @@ -612,15 +612,47 @@ button.secondary:hover { font-size: 12px; color: var(--text-muted); } -.section-header { - margin: 14px 0 10px; - padding-top: 10px; +.panel-section + .panel-section { + margin-top: 14px; border-top: 1px solid var(--line); } -.section-header:first-of-type { - margin-top: 4px; +.section-header { + display: flex; + align-items: center; + gap: 6px; + margin: 0; + padding: 10px 2px; + cursor: pointer; + list-style: none; + border-radius: 6px; + user-select: none; +} +.panel-section:first-of-type > .section-header { padding-top: 0; - border-top: none; +} +.section-header::-webkit-details-marker { + display: none; +} +.section-header::before { + content: ""; + flex: none; + width: 6px; + height: 6px; + margin: 0 3px 0 2px; + border-right: 2px solid var(--text-muted); + border-bottom: 2px solid var(--text-muted); + transform: rotate(-45deg); + transition: transform 0.12s ease; +} +.panel-section[open] > .section-header::before { + transform: rotate(45deg); +} +.section-header:hover .section-title { + color: var(--text); +} +.section-header:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; } .section-title { font-size: 12px; @@ -798,3 +830,46 @@ button.series-color-chip.active { font-size: 13px; } } + +.preset-row { + display: grid; + grid-template-columns: 1fr auto; + gap: 8px; +} +.preset-row button { + width: auto; + padding-left: 16px; + padding-right: 16px; +} +.row-size { + grid-template-columns: 1fr 1fr 5.5rem; +} +#mathtextHelp a { + color: var(--accent, inherit); + text-decoration: underline; +} + +[hidden] { + display: none !important; +} +.dpi-row { + display: grid; + grid-template-columns: 1fr; + gap: 8px; +} +.dpi-row.has-custom { + grid-template-columns: 1fr 1fr; +} +.dpi-row input { + margin-top: 0; +} +.save-actions { + grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); + align-items: stretch; +} +.save-actions button { + padding-left: 6px; + padding-right: 6px; + font-size: 13px; + white-space: nowrap; +} diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 8212281..482cddc 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -154,3 +154,81 @@ Phase 1 後より悪化した項目はなかった。このため、悪化の内 編集モードの実行(438 ms)は、設定変更の描画(デバウンス除く 216 ms)より約 220 ms 長い。仮想 FS からのファイル再読込と、生成スクリプトの全体実行(読込・整形・描画)を含むためとみられるが、内訳は未計測。短縮が必要になったら、Pyodide 内で読込・コンパイル・実行・PNG 変換の時間を分けて測ることから始める。 + +## Phase 3 後 + +### 計測条件 + +| 項目 | 内容 | +|---|---| +| 日付 | 2026-10-08 | +| 対象 | `020896b`(Phase 3: 日本語フォントを TrueType の Noto Sans JP へ変更、保存 DPI の選択(既定 300)、欧文フォント、クリップボードへのコピー、保存設定の変更でコードだけ更新)に、Phase 3 の計測行を追加した `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、ヘッドレス) | +| ネットワーク | 実ネットワーク(ベースラインと同じ。欧文フォントの初回取得と日本語フォントのコールド取得は jsDelivr からの実ダウンロード) | +| 配信 | ローカルの静的 HTTP サーバー(127.0.0.1、サブパスなし) | +| 反復 | 各項目 N=5。既存項目の計測方法は Phase 2 後と同じ(描画系の項目は 10,000 行 × 系列 5 本) | + +追加した項目(既存項目の方法は変えていない。新項目は、起動 → CSV 読込 → 系列5本設定 → 3秒待機の後に測る): + +- 「保存(PNG、120 dpi)」: `#saveDpi` を「任意」、`#saveDpiCustom` を 120 にして、既存の「保存」と同じ方法(クリック → ダウンロード開始)で測る。Phase 2 の固定 120 dpi と直接比較できる。 +- 「保存(PNG、300 dpi 既定)」: 既存の「保存」項目の計測条件。Phase 3 では既定が 300 dpi になったので、既存行の値は 300 dpi の値になる(Phase 2 後の値は 120 dpi)。別ページ・別コンテキストでも測り直して一致を確認した(下表の2行)。 +- 「欧文フォント選択 → 最初のプロット更新」: `#latinFont` を Arimo にして(「設定変更」と同じ `CHANGE_JS`。`input` イベント発行から新しい画像の表示まで。250 ms のデバウンスを含む)。(a) 初回は反復ごとに新規コンテキスト(Cache Storage も HTTP キャッシュも空)で jsDelivr から実取得、(b) 2回目以降は Cache Storage に Arimo がある状態。 +- 「クリップボードへのコピー」: `#copyPlotBtn` のクリックから `data-copy-state` が `done` になるまで(300 dpi 既定)。`clipboard-read` / `clipboard-write` の権限を付けたコンテキストで測る。 +- 「日本語フォント取得(コールドキャッシュ)」: 新規コンテキストで、ナビゲーション開始から `data-font-state` が `ready` になるまで。あわせて、Python の準備完了(`data-app-state=ready`)から `ready` までの追加待ち時間も記録する。 + +### 結果(ミリ秒、中央値 / 最小 / 最大) + +| 項目 | Phase 2 後 | Phase 3 後 | Phase 2 後比 | +|---|---:|---:|---| +| コールド起動 | 3,607 / 3,578 / 6,407 | 3,564 / 3,526 / 7,098 | 同等(-1%) | +| ウォーム起動 | 3,452 / 3,440 / 3,458 | 3,441 / 3,424 / 3,484 | 同等(-0%) | +| 初回ファイル読込 → 最初のプロット | 178 / 175 / 181 | 177 / 175 / 179 | 同等(-1%) | +| 設定変更 → プロット更新(デバウンス込み) | 466 / 460 / 477 | 471 / 468 / 473 | 同等(+1%) | +| 同上(デバウンス 250ms 除く) | 216 / 210 / 227 | 221 / 218 / 223 | 同等(+2%) | +| 保存(PNG、クリック → ダウンロード開始。既定の 300 dpi) | 246 / 211 / 253(120 dpi) | 405 / 364 / 408 | 悪い(+65%)。dpi の違い | +| 初回 .xlsx 読込 → 最初のプロット | 329 / 327 / 332 | 329 / 325 / 330 | 同等(0%) | +| 編集モードでの実行(Ctrl/Cmd+Enter → プロット更新) | 438 / 431 / 464 | 579 / 575 / 596 | 悪い(+32%)。dpi の違い | +| 保存(PNG、120 dpi)【新】 | 246 / 211 / 253 | 246 / 242 / 257 | 同等(0%) | +| 保存(PNG、300 dpi)【新。別測定】 | - | 403 / 369 / 412 | - | +| 欧文フォント選択 → プロット更新、初回(Arimo を jsDelivr から取得)【新】 | - | 468 / 456 / 472 | - | +| 同上、Cache Storage にあるとき【新】 | - | 467 / 462 / 469 | - | +| 参考: 欧文フォント初回の、選択 → `data-latin-font-state=ready`【新】 | - | 23 / 21 / 25 | - | +| クリップボードへのコピー(クリック → done、300 dpi)【新】 | - | 422 / 406 / 424 | - | +| 日本語フォント取得、コールド(ナビゲーション開始 → `font-state=ready`)【新】 | - | 3,622 / 3,606 / 3,623 | - | +| 同上、Python 準備完了からの追加待ち【新】 | - | 67 / 56 / 77 | - | + +コールド起動の最大値は1回目の反復のみ(キャッシュが冷えていたため)。2回目以降は約3.6秒。 + +### 要件の確認 + +要件「設定変更後 1 秒以内の再描画(10,000 行 × 5 系列)」: 設定変更 → プロット更新は、デバウンス 250 ms を含めて中央値 471 ms(最大 473 ms)で、**満たしている**。編集モードの実行も、300 dpi の既定で中央値 579 ms(最大 596 ms)で 1 秒以内。欧文フォントの選択からの更新も 468 ms で、同様に 1 秒以内。 + +### 分析 + +悪化した項目は2つで、どちらも保存 DPI の既定が 120 → 300 になったことによる(起動・再描画・xlsx 読込は Phase 2 後と同等)。 + +- **保存(PNG)+65%**: 300 dpi は 120 dpi の 6.25 倍の画素数(8×6 inch で 2400×1800 = 4.3 MP に対し 960×720 = 0.7 MP)。同じ設定で 120 dpi に戻した値は 246 ms(Phase 2 後の 246 ms と同じ)なので、ランタイムや生成スクリプトの増加による劣化はない。画素数が 6.25 倍になっても時間は 1.6 倍にとどまる。固定費(スクリプトの生成と実行、系列の描画)が大きく、画素数に比例する部分(Agg のラスタライズと PNG のエンコード)は一部のため。 +- **編集モードの実行 +32%**: 編集モードのスクリプトは、末尾の `fig.savefig(...)` を含めて全体を実行する。Phase 3 から生成コードの `savefig` が既定 300 dpi になったので、その分の PNG 書き出しが増えた。DPI を 120 にして同じ計測をすると 439 / 425 / 443 ms で、Phase 2 後(438 ms)と同じ(確認用に3回測った値)。 + +悪化していない項目の補足: + +- **日本語フォントの変更(16.5 MB の OTF → 5.7 MB の TTF、転送は約 3.2 MB の brotli)**: コールドキャッシュでも、Python の準備完了までにフォント取得がほぼ終わる(ナビゲーション開始 → `ready` が 3,622 ms、準備完了からの追加待ちは 67 ms)。取得は Pyodide の起動と並行して進むため、起動の体感を延ばさない(コールド起動 3,564 ms は Phase 2 後と同等)。以前は 16.5 MB を取得していたので、初回訪問のダウンロード量は約 1/3 になった。2回目以降は Cache Storage から読む点は変わらない。 +- **欧文フォントの初回選択(Arimo)**: 初回(取得あり)と2回目以降(Cache Storage から)で差がない(468 ms と 467 ms)。選択からフォントの登録完了までが約 23 ms で、250 ms のデバウンスの間に終わるため、描画は取得を待たない。フォントが約 0.3〜0.5 MB と小さいことと、デバウンスと並行して取得を始める作りのため。 +- **クリップボードへのコピー**: 422 ms で、300 dpi の保存(405 ms)とほぼ同じ。PNG の生成が大半で、クリップボードへの書き込みは十数 ms 程度とみられる。 +- 保存設定(`save.*`)の変更は画像を再描画せず、コードだけ更新する(計測行は設けていない)。 + +### 一回限りの確認: ラスタ画像の画素数の上限(`formats.MAX_RASTER_PIXELS` = 100,000,000) + +実アプリ(Chromium、Pyodide)で、上限付近の PNG 保存を試した。10,000 行 × 5 系列のデータで、図の大きさ(inch)と DPI を設定してから保存ボタンを押した。メモリは、Pyodide の WebAssembly メモリのバイト数(`WebAssembly.instantiate` の結果から `exports.memory` を捕まえて `buffer.byteLength` を読んだ)。保存前と保存直後の値。 + +| 設定 | 画素数 | 結果 | 時間 | 出力ファイル | wasm メモリ(保存前 → 後) | +|---|---:|---|---:|---|---:| +| 8×6 inch、1200 dpi(9600×7200) | 69.1 MP | 成功 | 2.1 秒 | 4.2 MB | 108 MB → 442 MB | +| 10×10 inch、999 dpi(9990×9990) | 99.8 MP | 成功 | 3.2〜3.3 秒 | 6.8 MB | 130 MB → 599 MB | +| 10×10 inch、1000 dpi(10000×10000) | 100.0 MP(上限ちょうど) | 成功 | 3.2 秒 | 6.8 MB | 130 MB → 600 MB | + +- どれも、メモリ不足やクラッシュは起きなかった。コンソールエラーもない。画像の大きさは、ファイルを開いて指定どおりであることを確かめた。 +- 上限付近では、RGBA の画素バッファ(1画素 4 バイト、100 MP で約 400 MB)が wasm のメモリに載るため、wasm メモリは約 600 MB まで広がった。wasm のメモリは縮まないので、保存後もこの大きさのまま残る。JS ヒープ(`performance.memory`)は約 93〜98 MB で変わらない。 +- この Mac(24 GB)の結果。メモリの少ない端末(スマートフォンなど)では、600 MB 級の確保が失敗する可能性があるが、今回は確かめていない。上限(1億画素)の変更は不要と判断し、触っていない。 diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index d2ee263..2ae10ae 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -39,6 +39,15 @@ - 「実装ファイルは `index.html` 単体」「`index.html` を開くだけで動く」は廃止し、GitHub Pages 配信を前提にする。 - §12 の作業対象は絶対パスで書かれており、しかも実際の場所と合っていない。絶対パスはやめて「このリポジトリ配下のみ」と書く(ローカル環境のパスを公開リポジトリに載せないため)。 +### 2.3 Phase 3 の決定事項(2026-10-08 にユーザーが決定) + +- U1–U3 は 10 章の既定案のとおりにする。 +- 図サイズの単位を切り替えたら、入力欄の数値を換算する(図の大きさは変えない。例: 8 inch → 20.32 cm)。 +- カラーパレットを切り替えたら、すべての系列を、系列の順に新しいパレットの色で塗り直す(個別に選んだ色も上書きする)。 +- クリップボードにコピーする画像(C5)は、保存 DPI と同じ解像度の PNG にする。背景透過は保存設定に従う。 +- 左パネルを「データ読み込み」「系列」「軸・目盛」「体裁(フォント・サイズ・プリセット)」「保存」の区分に整理し、各区分を折りたためるようにする。「注釈・解析」の区分は、中身が入る Phase 5 まで作らない。 +- 日本語フォントを、Noto Sans CJK JP(OTF、CFF 形式、約16 MB)から、同じデザインの TrueType 版 Noto Sans JP(約5.7 MB)に替える。matplotlib は CFF 形式のフォントを `pdf.fonttype=42` で正しく埋め込めない(TrueType 用の FontFile2 に CFF を入れ、PDF が壊れる)ため。文字幅は同じで、見た目は変わらない。ハングルと中国語専用の漢字は含まれない(JIS 第1〜第4水準は含む)。 + --- ## 3. 現状の構成(2026-10-07 時点) @@ -145,9 +154,9 @@ docs/REQUIREMENTS.md 本書 | ID | 優先度 | 要件・受け入れ条件 | |---|---|---| -| C1 | Must | PNG/JPG の保存 DPI を指定できる(72 / 150 / 300 / 600、または任意の値)。既定値は 300(未決事項 U1) | -| C2 | Must | 図のサイズを inch / cm / mm で指定できる。PDF は `pdf.fonttype=42` で書き出し、Illustrator などで文字を編集できるようにする。SVG では、文字をテキストのまま残すかパスに変換するかを選べる | -| C3 | Should | 欧文フォントを選べる(Arial 互換、Times 互換など。未決事項 U3)。日本語には Noto Sans CJK JP を組み合わせる | +| C1 | Must | PNG/JPG の保存 DPI を指定できる(72 / 150 / 300 / 600、または任意の値)。既定値は 300(U1 で決定) | +| C2 | Must | 図のサイズを inch / cm / mm で指定できる。PDF は `pdf.fonttype=42` で書き出し、Illustrator などで文字を編集できるようにする。SVG では、文字をテキストのまま残すかパスに変換するかを選べる。※ Type 42 で正しく埋め込めるのは TrueType 形式のフォントだけなので、日本語フォントを TrueType 版の Noto Sans JP に替えた(2.3 を参照) | +| C3 | Should | 欧文フォントを選べる(Arial 互換、Times 互換など。U3 で決定)。日本語には Noto Sans JP(Noto Sans CJK JP と同じデザインの TrueType 版。2.3 を参照)を組み合わせる | | C4 | Should | スタイルプリセットを用意する。例: 「論文1段組(幅8.5cm、8pt)」「論文2段組(幅17cm)」「スライド16:9」。選ぶと、図のサイズ、フォントサイズ、線幅、マーカーサイズをまとめて設定する。カラーパレット(UD カラー、tab10、グレースケールなど)も切り替えられる | | C5 | Should | プレビュー中の図を、クリップボードに画像としてコピーできる | | C6 | Could | ラベルで数式・上付き・下付き(matplotlib の mathtext。例: `m$^2$`、`$\alpha$`)を使えることを画面上で案内する | @@ -236,9 +245,9 @@ docs/REQUIREMENTS.md 本書 | ID | 論点 | 既定案 | |---|---|---| -| U1 | 保存 DPI の既定値 | 300 | -| U2 | 図サイズの既定の単位 | 既定は inch(現行の 8×6 を維持)。cm と mm も選べる | -| U3 | 追加する欧文フォント | Arimo(Arial 互換)、Tinos(Times 互換)。どちらも Apache 2.0 ライセンスで、バージョン固定の CDN から取得する | +| U1 | 保存 DPI の既定値 | **決定(2026-10-08)**: 300 | +| U2 | 図サイズの既定の単位 | **決定(2026-10-08)**: 既定は inch(現行の 8×6 を維持)。cm と mm も選べる | +| U3 | 追加する欧文フォント | **決定(2026-10-08)**: Arimo(Arial 互換)、Tinos(Times 互換)。バージョン固定の CDN から取得する。※ ライセンスは、どちらも SIL Open Font License 1.1(当初 Apache 2.0 と書いたのは誤り。現行版は OFL 1.1) | | U4 | グラフ種別を追加する順番 | 5.6 の G7 → G8 の順 | | U5 | scipy を導入するか | 任意関数のフィッティング(G10)を使うときだけ読み込む | | U6 | GitHub Actions を使うか(F5) | 使う | diff --git a/index.html b/index.html index 65d7e56..d54c59f 100644 --- a/index.html +++ b/index.html @@ -42,225 +42,278 @@
-
-
- - + + +
-
-
-
- 描画系列 - +
+ +
-
-
系列ごとにX/Y列・色・線幅・線種・点サイズ・第2軸使用を設定できます。
-
-
- - -
+
+
+ + +
+
-
-
- - +
+
+ 描画系列 + +
+
+
系列ごとにX/Y列・色・線幅・線種・点サイズ・第2軸使用を設定できます。
-
- -
-
- - + -
- - -
+
+ 軸・目盛 +
+
+ + +
-
-
- - +
+
+ + +
+
+ + +
-
- - + + -
-
- - -
+
+ タイトル・軸ラベル・凡例名では、$ で囲んだ部分が数式になります。例: m$^2$(上付き)、H$_2$O(下付き)、$\alpha$(ギリシャ文字)。$ を文字として書くときは \$ とします。 + 書き方の詳細(matplotlib) +
-
-
- - +
+ +
-
- - + +
+
+ + +
+
+ + +
-
- -
- - -
+ -
-
- - +
+
+ + +
+
+ + +
-
- -
-
- -
- - -
+
-
-
- - +
+ 体裁 +
+
+ +
+ + +
+
図のサイズ・フォントサイズ・線幅・点サイズをまとめて設定します。
-
- - + +
+ +
-
-
-
- - + +
+ + +
英数字と記号を、選んだフォントで書きます。日本語の文字は Noto Sans JP で書きます。選んだときに、フォント(約 0.3〜0.5 MB)を取得します。
-
- - + +
+ +
-
-
-
- - + +
+
+ + +
+
+ + +
+
+ + +
-
- - + +
+ +
-
-
-
保存設定
-
+
+
+ + +
+
+ + +
+
-
- - -
+
+
+ + +
+
+ + +
+
+
+
-
- - -
+
+ 保存 +
+
+ + +
-
-
+
-
- + +
+ +
+ + +
+
PNG・JPG の保存と、クリップボードへのコピーに使います(プレビューは 100 dpi で表示します)。
+
+ + + + + +
+ + +
+ +
+
-
+
+