この記事のコード解説 ― SVGの図・対数グラフ・計算機の作り方
記事「図でわかる通信のしくみ」の図・グラフ・計算機を、実際のコードで解説します。データから SVG を描く、log10 で棒の長さをそろえる、input イベントで即時計算する、の3つです。
本編の記事には、画像ファイルを1枚も使っていません。図もグラフも計算機も、すべてコードで作っています。ここでは、その作り方を3つに分けて解説します。
1. データから SVG の図を描く
まず、図に出す中身を配列(データ)として書きます。図の見た目と中身を分けておくと、層を1つ足すときもデータに1行足すだけで済みます。
// 図の中身を「データ」として先に書いておく
const osi = [
{ n: 7, name: 'アプリケーション層', ex: 'HTTP・DNS・SMTP' },
{ n: 4, name: 'トランスポート層', ex: 'TCP・UDP(ポート番号)' },
{ n: 3, name: 'ネットワーク層', ex: 'IP(IPアドレス)・ルータ' },
// …
];
const ROW = 46; // 1段の高さ(px)次に、その配列を map で1段ずつ図形に変えます。i * ROW で、i 段目を下へずらしています。
<svg viewBox="0 0 640 340" role="img" aria-labelledby="fig1t">
<title id="fig1t">OSI参照モデル7層とTCP/IP4層の対応図</title>
{osi.map((l, i) => (
<g transform={`translate(0,${i * ROW})`}>
<rect x="0" y="2" width="420" height={ROW - 6} rx="6" />
<text x="14" y="27">{`L${l.n}`}</text>
<text x="50" y="27">{l.name}</text>
</g>
))}
</svg>| 部品 | 役割 |
|---|---|
viewBox | 図の中で使う座標の広さ。画面の幅が変わっても、この比率のまま拡大・縮小される |
<g transform> | 中の図形をまとめて動かす。1段ぶんの四角と文字を一緒にずらせる |
<rect> / <text> | 四角形と文字。rx は角の丸み |
role="img" と <title> | 読み上げソフトに「これは図で、内容はこう」と伝える |
静的サイトを作るツール(ビルドのときに HTML を作るもの)では、この map はページを作るとき(ビルド時)に1回だけ動きます。読者のブラウザに届くのは、できあがった SVG だけです。だから JavaScript を切っていても図は表示されます。
2. 差が大きい数字を、対数で棒にする
1GB を送る時間は、10Mbps で 800秒、10Gbps で 0.8秒。1000倍の差があります。そのまま棒の長さにすると、短い棒は1ピクセルにもなりません。そこで Math.log10(常用対数)を使います。
// 1GB(= 8,000Mビット)を送る秒数
const sec = 8000 / l.mbps; // 800, 80, 8, 0.8
// そのまま長さにすると 800 と 0.8 で1000倍ちがい、短い棒が見えない
// log10 で「桁」をそろえてから長さにする
const w = 40 + (Math.log10(sec) + 0.2) / (Math.log10(800) + 0.2) * 380;対数は基本情報でも出てきます。「10倍ごとに同じ幅だけ伸びる」目盛りは、音の大きさ(デシベル)や地震の規模(マグニチュード)と同じ考え方です。
3. 打つそばから答えが出る計算機
計算機は、入力が変わるたびに計算し直すだけのしくみです。
function calc() {
const mb = Number($('cSize').value) * Number($('cUnit').value);
const mbps = Number($('cSpeed').value) * Number($('cSpUnit').value);
const eff = Number($('cEff').value) / 100;
// 入力が空や0のときは計算しない(0で割るのを防ぐ)
if (!(mb > 0 && mbps > 0)) { out.textContent = '—'; return; }
const sec = (mb * 8) / (mbps * eff); // 公式そのまま
out.textContent = `${sec} 秒`;
}
// 'input' は値が変わるたびに起きるイベント。打つそばから答えが変わる
['cSize', 'cUnit', 'cSpeed', 'cSpUnit', 'cEff']
.forEach(id => $(id).addEventListener('input', calc));Number(...):入力欄の値は文字列なので、数に直してから計算するinputイベント:1文字打つたび、スライダーを動かすたびに起きる。changeだと入力欄から離れたときだけaria-live="polite":答えの欄に付けると、読み上げソフトが変わった答えを読んでくれる
おまけ:図の文字の重なりをコードで調べる
本編を作ったとき、「ネットワーク」と「インタフェース層」の2行が少し重なっていました。目で探すのは大変なので、ブラウザで全部の文字の四角形を比べるコードを動かして見つけました。
// 図の文字どうしが重なっていないかを調べる(ブラウザのコンソールで実行)
const ts = [...svg.querySelectorAll('text')].map(t => {
const b = t.getBBox(); // 文字が占める四角形
const m = t.getCTM(); // translate でずらした量
return { x1: b.x + m.e, x2: b.x + b.width + m.e,
y1: b.y + m.f, y2: b.y + b.height + m.f };
});
// 2つの四角形が重なる条件:左右も上下も、たがいに相手の範囲に入っている
const hit = (a, b) => a.x1 < b.x2 && b.x1 < a.x2 && a.y1 < b.y2 && b.y1 < a.y2;「左右も上下も、たがいに相手の範囲に入っている」ときだけ重なります。ゲームの当たり判定(矩形の衝突判定)と同じ考え方です。
問題と解説
Q1図を画像ファイル(PNG)ではなく SVG のコードで描くと、何がよいですか。2つ挙げてください。
答え:拡大してもぼやけないこと。文字が本文と同じように検索・読み上げ・翻訳できること。
SVG は「点の集まり」ではなく「図形の命令」なので、どの大きさでもくっきり描き直されます。中の文字もテキストのままなので、スクリーンリーダーや翻訳拡張機能(HoverTranslator など)でも読めます。
Q2棒グラフの長さを log10 で決めたのはなぜですか。
答え:一番長い値と一番短い値が1000倍ちがい、そのままだと短い棒がほとんど見えなくなるから。
log10 は「10を何回掛けたか(桁)」を返します。800→約2.9、0.8→約−0.1 になり、差が「桁の差」に縮まります。そのかわり、棒の長さの比は実際の比とは違うので、図の説明にそのことを書いておくのが大事です。
Q3計算機で「0で割る」のを防いでいるのはどの行ですか。
答え:if (!(mb > 0 && mbps > 0)) の行。
回線速度が 0 や空のまま割り算をすると Infinity(無限大)や NaN(数ではない)になります。計算の前に「使える値か」を確かめて、だめなら「—」を出して終わります。
まとめ
- 図の中身はデータ(配列)に、見た目は
mapと SVG に分ける - 差が大きい数字は log10 でそろえる。そのことを図の説明に書く
- 計算機は
inputイベントで計算し直すだけ。0で割る前に値を確かめる