プログラミング

この記事のコード解説 ― SVGの図・対数グラフ・計算機の作り方

記事「図でわかる通信のしくみ」の図・グラフ・計算機を、実際のコードで解説します。データから SVG を描く、log10 で棒の長さをそろえる、input イベントで即時計算する、の3つです。

目次
  1. 1. データから SVG の図を描く
  2. 2. 差が大きい数字を、対数で棒にする
  3. 3. 打つそばから答えが出る計算機
  4. おまけ:図の文字の重なりをコードで調べる
  5. 問題と解説
  6. まとめ
この記事は「図でわかる通信のしくみ」の付属記事です。先に本編を見てから読むと、どのコードがどの図になっているかが分かりやすくなります。

本編の記事には、画像ファイルを1枚も使っていません。図もグラフも計算機も、すべてコードで作っています。ここでは、その作り方を3つに分けて解説します。

1. データから SVG の図を描く

まず、図に出す中身を配列(データ)として書きます。図の見た目と中身を分けておくと、層を1つ足すときもデータに1行足すだけで済みます。

JavaScript
// 図の中身を「データ」として先に書いておく
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 のテンプレート
<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(常用対数)を使います。

JavaScript
// 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;
そのままの長さと、対数にした長さの比較そのまま(比例)対数(log10)
上:そのままだと 0.8秒の棒はほぼ見えない。下:対数にすると4本とも見える(長さの比は実際の比ではなくなる)。

対数は基本情報でも出てきます。「10倍ごとに同じ幅だけ伸びる」目盛りは、音の大きさ(デシベル)や地震の規模(マグニチュード)と同じ考え方です。

3. 打つそばから答えが出る計算機

計算機は、入力が変わるたびに計算し直すだけのしくみです。

JavaScript
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));

おまけ:図の文字の重なりをコードで調べる

本編を作ったとき、「ネットワーク」と「インタフェース層」の2行が少し重なっていました。目で探すのは大変なので、ブラウザで全部の文字の四角形を比べるコードを動かして見つけました。

JavaScript
// 図の文字どうしが重なっていないかを調べる(ブラウザのコンソールで実行)
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(数ではない)になります。計算の前に「使える値か」を確かめて、だめなら「—」を出して終わります。

まとめ

← 本編「図でわかる通信のしくみ」へ / コード解説:基本文法

← 記事一覧へ戻る