数式を入れたら、その場でグラフになってほしい。それだけのことなのに、意外と手頃な選択肢がありません。
高機能なグラフ電卓サービスは重かったり、アカウントが要ったり。表計算ソフトで無理やり描くと、範囲や刻みの設定に手間がかかる。「y = sin(x) を見たい」の 1 分の作業に、そこまでかけたくない。
PlotPad は、数式を打つと即座にグラフを描く関数グラフ電卓です。複数の式を重ねたり、ドラッグで動かしたり、導関数を並べたり。全部ブラウザの中で完結します。
#🎯 「式 → グラフ」を最短にする
やりたいことは単純です。x^2 - 3 と打ったら放物線が出る。sin(x) と打ったら波が出る。この最短経路を、余計な設定なしで通したい。
そのために、式の入力欄に打った瞬間から描画が更新される作りにしました。範囲や刻みは自動で「きりの良い」値が選ばれ、必要なら後から動かせる。まず絵が出て、それから調整する、という順番です。
#🏗️ 式の解釈は mathjs に任せる
数式を解釈して計算する部分——sin(x^2) + 1 のような文字列を「x を入れたら値が返る関数」に変える——は、ゼロから書くと構文解析器を実装することになります。ここは車輪の再発明をせず、mathjs に委譲しました。
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 段構えにしました。
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 のような切りの良い間隔であってほしい。
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 をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。