▍AI Dev Lab
PlotPad

PlotPad ができるまで — ブラウザで動く関数グラフ電卓

関数グラフ描画ツールの開発ログ。 式の解析と記号微分は mathjs に任せ、 1 ピクセル刻みのサンプリング描画、 発散点での線の切り方、 ドラッグ移動とカーソル基準のズームを自前で実装した記録です。

数式を入れたら、その場でグラフになってほしい。それだけのことなのに、意外と手頃な選択肢がありません。

高機能なグラフ電卓サービスは重かったり、アカウントが要ったり。表計算ソフトで無理やり描くと、範囲や刻みの設定に手間がかかる。「y = sin(x) を見たい」の 1 分の作業に、そこまでかけたくない。

PlotPad は、数式を打つと即座にグラフを描く関数グラフ電卓です。複数の式を重ねたり、ドラッグで動かしたり、導関数を並べたり。全部ブラウザの中で完結します。


#🎯 「式 → グラフ」を最短にする

やりたいことは単純です。x^2 - 3 と打ったら放物線が出る。sin(x) と打ったら波が出る。この最短経路を、余計な設定なしで通したい。

そのために、式の入力欄に打った瞬間から描画が更新される作りにしました。範囲や刻みは自動で「きりの良い」値が選ばれ、必要なら後から動かせる。まず絵が出て、それから調整する、という順番です。


#🏗️ 式の解釈は mathjs に任せる

数式を解釈して計算する部分——sin(x^2) + 1 のような文字列を「x を入れたら値が返る関数」に変える——は、ゼロから書くと構文解析器を実装することになります。ここは車輪の再発明をせず、mathjs に委譲しました。

ts
import { compile, derivative } from "mathjs"

export function compileExpr(expr: string): CompiledExpr {
 const trimmed = expr.trim
 if (!trimmed) return { expr, eval: => NaN, error: null }
 try {
 const c = compile(trimmed)
 c.evaluate({ x: 1 }) // 1 点評価して実行時に壊れないか確認

 const fn = (x: number): number => {
 try {
 const v = c.evaluate({ x })
 return typeof v === "number" && Number.isFinite(v) ? v : NaN
 } catch {
 return NaN
 }
 }
 return { expr, eval: fn, error: null }
 } catch (e) {
 return { expr, eval: => NaN, error: (e as Error).message || "式を解釈できません" }
 }
}

compile で式を一度コンパイルしておき、以降は evaluate({ x }) を呼ぶだけ。サンプリングでは同じ式を数百回評価するので、毎回パースし直さずコンパイル済みを使い回すのが効きます。

ポイントは 2 段のエラー処理です。まず compile の時点で構文エラーを捕まえ、さらに各点の評価でも try/catch して、log(-1) のような定義域の外では NaN を返す。これで「式は正しいが、この x では値がない」を描画側が扱えるようになります。


#📉 導関数:記号微分できなければ数値微分

PlotPad は元の関数に加えて、その導関数(傾きの変化)も重ねて描けます。これが地味に便利で、「どこで増減が切り替わるか」が視覚的に分かります。

導関数の求め方は 2 段構えにしました。

ts
export function derivativeOf(expr: string): CompiledExpr {
 try {
 const d = derivative(expr.trim, "x").toString // 記号微分

 return compileExpr(d)
 } catch {
 // 記号微分できなければ数値微分(中心差分)

 const base = compileExpr(expr.trim)
 if (base.error) return { expr: "", eval: => NaN, error: base.error }
 const h = 1e-5
 return {
 expr: "d/dx",
 eval: (x: number) => {
 const a = base.eval(x + h)
 const b = base.eval(x - h)
 if (!Number.isFinite(a) || !Number.isFinite(b)) return NaN
 return (a - b) / (2 * h) // 中心差分

 },
 error: null,
 }
 }
}

まず mathjs の記号微分を試します。x^2 なら 2*x という式が返り、それをコンパイルすれば正確な導関数が得られる。しかも導関数の「式そのもの」を画面に表示できます。

記号微分が効かない複雑な式では、数値微分(中心差分 (f(x+h) − f(x−h)) / 2h)にフォールバックします。式は出せませんが、グラフは描ける。「できるなら正確に、無理でも近似で必ず描く」という二段構えです。


#📏 目盛りを「きりの良い」値にする

グラフの軸の目盛りが 0.37, 0.74, 1.11... のような半端な数だと、途端に読みにくくなります。目盛りは 1, 2, 5, 10 のような切りの良い間隔であってほしい。

ts
export function niceStep(rawStep: number): number {
 if (rawStep <= 0 || !Number.isFinite(rawStep)) return 1
 const mag = Math.pow(10, Math.floor(Math.log10(rawStep)))
 const norm = rawStep / mag
 const nice = norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10
 return nice * mag
}

「理想の目盛り間隔」を計算したら、それを一番近い 1 / 2 / 5 × 10ⁿ に丸めます。桁(mag)を対数で求め、その中での位置(norm)を見て 1・2・5・10 のどれかに寄せる。この小さな関数があるだけで、どんなズーム倍率でも軸が読みやすく保たれます。グラフツールの「見やすさ」は、こういう地味な整形の積み重ねでできています。


#😓 苦労したところ

#不連続・発散をどう描くか

1/x や tan(x) は、ある点で値が無限に飛びます。素直に点を線でつなぐと、飛んだ箇所に縦線が突き抜けて、まるでそこに値があるように見えてしまう。

対策は、NaN や急激に飛んだ点で線を切ること。評価が NaN を返した区間は描かず、値が不連続に大きくジャンプした箇所も分割する。「つながっていないものを、つなげて描かない」——正しいグラフのために、あえて線を引かない判断が要りました。

#入力途中のエラーで画面を荒らさない

式を打っている途中は、sin( のような未完成の状態が必ず発生します。ここで毎回赤いエラーを出すと、入力体験が最悪になる。

そこで、構文エラーは静かに保持して描画を止めるだけにし、式が有効になった瞬間に描く。エラーメッセージは、明らかに間違ったまま手が止まったときにだけ控えめに出す。打っている最中は邪魔をしないのが、入力ツールの基本だと考えています。


#🔭 今後の拡張

  • 交点・極値の自動表示 — 2 式の交わる点や、山・谷の座標を出す
  • 積分(面積)の可視化 — 指定区間の面積を塗る
  • 媒介変数・極座標 — x = cos(t), y = sin(t) のような表現
  • アニメーション — パラメータをスライダーで動かして曲線が変わる様子を見る

#💡 このサービスから言えること

車輪の再発明をしない判断が、質を上げることもある。 数式の構文解析は難しく、自前で書けば必ずバグが残ります。mathjs のような枯れたライブラリに任せることで、その労力を「不連続の描画」「目盛りの整形」「入力体験」といった、実際に使い心地を左右する部分に回せました。何を作り、何を借りるかの見極めが、限られた時間での質を決めます。

もうひとつは、正しく描くために「描かない」判断です。不連続点で線を切る、入力途中はエラーを出さない——どちらも「余計なことをしない」ことで正しさと使いやすさを得ています。グラフツールは足し算より引き算で良くなる場面が多い、というのがこのサービスからの実感です。

[ ./next_action ]

読んだら、 PlotPad を実際に動かす。

この開発ログは PlotPad をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ShadowTwin

立体パズルの別解を作る:同じ影と最少の積み木

ShadowTwinの開発記録。3方向の図から別々の最少配置を2つ探す仕組み、5問の独立全探索、スマホの横並び比較、保存・再編集と入力検査を解説します。実照明の計算や能力診断ではありません。

read log →
SlitCard

スリットアニメーションを作る:4コマの合成と透明原稿の設計

SlitCardの開発記録。4コマの帯を交互に合成し、すき間と黒帯で別のコマを見せる仕組みを解説。同縮尺の台紙・透明シートSVG、50mm確認線、手動プレビュー・初期非表示、再編集と実出力検査を整理します。

read log →
WindowLight

日当たりシミュレーションを作る:窓の光を床へ投影する幾何モデル

WindowLightの開発記録。太陽位置近似から窓の4隅を床へ投影し、凸多角形を部屋で切り取る設計を解説。国立天文台の16時刻との限定照合、大気差・低高度・実際の採光との違い、SVGと設定の再編集を整理します。

read log →
GridLights

電源構成の比較を作る:割合と実量を100個の電球で読み解く

電源構成比較GridLightsの開発記録。12か国の年次発電データを最大剰余法で100個の電球に変換し、割合とTWhを比較する設計を解説。欠損を0補完しない7分類、予想と実値、出典付きSVGと実データ検証を整理します。

read log →
SoundLoom

音のお絵かきを作る:実WAVから絵を測り直す設計

音のお絵かきの開発記録。描画を正弦波のPCMとWAVへ変換し、保存音声の周波数をGoertzel法で測って比較する仕組みを解説。音量・測定窓・量子化の限界と、編集・離脱時の古い結果を抑止する検証を整理します。

read log →
MazeLoom

宝探し迷路を作る:寄り道まで含めた最短ルートの探索

宝探し迷路の開発記録。通路を木として生成し、現在地と3つの宝の取得状態を組み合わせて最短ルートを探す仕組みを解説。問題コードの版固定、画面と解答付きPDFの一致、手数と印刷の検証を整理します。

read log →