はじめに
昔の話になりますが、京都大学の前野悦輝先生が立体周期表「エレメンタッチ(ElemenTouch)」が発表されたとき、私はその斬新な意図に感銘を受けました。
漠然とした理解の困難さが解決したような気がしました。
それからずいぶん経ちますが、メジャーな存在とは言い難く、その良さが広く認知されているとは思いません。なぜか?立体だからです。印刷物に載せられないんです。いつでも参照するわけにもいかないんです。
このエレメンタッチ、なんとか平面に持ち込んで手軽に持ち歩ける方法はないかと思って考えました。結論。印刷物での2D表示では無理。そりゃそうだ。
でも、アプリならできる。
そこで、自分なりに内容をかみ砕き、インタラクティブなアプリに仕上げました。
価数(他の原子との結びつきやすさ)でそろえた横長の周期表アプリ 元素ベルト(Element Belt) です。
- アプリ: https://app.emuyn.net/element_belt/ (英語版: https://app.emuyn.net/element_belt/en)
- リポジトリ: https://github.com/EMUYN-LLC/element_belt (CC BY 4.0)
前野先生のご了承のもと公開しています。
論文は末尾に引用してあります。
この記事では、次の実装ポイントを順に紹介します。
- ビルドレス単一 HTML という構成
- 価数列モデル ― 「Ca と Cd が同じ列」になる仕組み
- らせんの高さ配置
- 魚眼レイアウト
- 横スクロールで帯全体が上下する「螺旋」演出
- 電子配置を構成原理で計算する(+例外 20 元素)
- 静的辞書だけで多言語化する(
/ja/en) - アクセシビリティ(デジタル庁デザインシステム参照)
1. ビルドレス単一 HTML
index.html ひとつで完結します。ダウンロードしてブラウザで開けばそのまま動く。npm も webpack も Vite もありません。
index.html アプリ本体(これだけで動く)
favicon.svg
README.md
LICENSE
教育用の小さなアプリでは、この「1 ファイルで完結」が配布・改変・保守のすべてで効きます。ソースを読めば全部そこにある、というのは学習教材としても素直です。
2. 価数列モデル ― 「Ca と Cd が同じ列」になる仕組み
このアプリの核です。ふつうの周期表は「族」で縦にそろえますが、元素ベルトは 価数 (酸化数) でそろえます。すると s+p の 8 列に、d・f ブロックの両端が折り込まれるという、独特のグループ分けになります。
たとえば 2 価の列は Be Mg Ca Sr Ba Ra に加えて Zn Cd Hg Cn が同居し、Ca と Cd が同じ列に来ます。
各元素 z がどの価数列に属するかを返すのが eleCol() です。
function eleGroup(z) {
if (z === 1) return 1;
if (z === 2) return 18;
if (z >= 3 && z <= 10) return [1, 2, 13, 14, 15, 16, 17, 18][z - 3];
if (z >= 11 && z <= 18) return [1, 2, 13, 14, 15, 16, 17, 18][z - 11];
if (z >= 19 && z <= 36) return z - 18;
if (z >= 37 && z <= 54) return z - 36;
// …(La/Lu/Ac/Lr は 3、Ce/Th は 4、f 中間は 0 として別扱い)
if (isFmid(z)) return 0;
if (z >= 72 && z <= 86) return z - 68;
if (z >= 104 && z <= 118) return z - 100;
return 0;
}
function eleCol(z) {
const g = eleGroup(z);
if (isFmid(z)) return "f" + (z <= 70 ? z - 59 : z - 91); // f ブロック中間
if ([57, 71, 89, 103].includes(z)) return "v3"; // La/Lu/Ac/Lr → 3 価
if (g === 1) return "v1";
if (g === 2) return "v2";
if (g >= 13) return "v" + (g - 10); // 13→v3, 14→v4 … 18→v8(貴ガス)
if (g === 3) return "v3";
if (g === 4) return "v4";
if (g === 12) return "v2"; // 亜鉛族を 2 価列へ折り込む
return "d" + g; // d ブロック中間(遷移金属)
}
ポイントは、12 族(Zn/Cd/Hg)を v2 に、3・4 族相当を v3/v4 に折り込むところ。ここで Ca(2 族)と Cd(12 族)が同じ v2 になります。d ブロックの中間(V〜Cu など)と f ブロックの中間は、円筒からはみ出す独自の列として d* f* に分けています。
各列のメンバーは、起動時に一度だけ集めておきます。
const COLMEM = {};
for (let z = 1; z <= 118; z++) {
(COLMEM[eleCol(z)] ??= []).push(z);
}
3. らせんの高さ配置
上下に並ぶ「同じ価数列の仲間」を、どの高さに置くか。エレメンタッチが三重の円筒にらせん状に元素を並べるのにならって、「周期+周期内の相対位置」を高さにしています。
const PSTART = [1, 3, 11, 19, 37, 55, 87],
PLEN = [2, 8, 8, 18, 18, 32, 32];
function heightOf(z) {
const p = periodOf(z);
return p - 1 + (z - PSTART[p - 1]) / PLEN[p - 1];
}
これで、同じ周期帯の関連元素が同じ高さにそろって横のつながりがきれいになり、周期の境目に自然な隙間ができます。実際の描画では、この高さを基準にしつつ、セルが重ならないように中央(主帯)から上下へ最小間隔を確保して押し出しています。
4. 魚眼レイアウト
横長の帯なので、画面中央に近い列ほど大きく見せると操作しやすく、少し 3D っぽい感じになります。スクロール位置 t(列単位)からの距離で倍率を決める、シンプルな魚眼です。
const mag = (d) => {
d = Math.abs(d);
return d >= 2.5 ? 1 : 1.5 - 0.2 * d; // 中央1.5・±1で1.3・±2で1.1・以遠1.0
};
各列の幅を mag() で決めて左から積み上げ、連続 index t の「列の中心」が画面中央 cx に来るように全体をシフト。列ごとに transform: translateX(...) scale(sx, sy) を当てています(横は最大 1.5 倍、縦はその 0.4 掛けで最大 1.2 倍)。
5. 横スクロールで帯全体が上下する「螺旋」演出
エレメンタッチが「らせんを降りていく」イメージを、横スクロールに連動した帯全体の上下動で表現しています。左端で上、右端で下へ。端の列を画面外に逃がすことで、中央の見せたい列を表示領域に収める役割も兼ねます。
function update() {
layoutFisheye();
// 螺旋の上下動:左端(frac=0)で上へ(-AMP)、右端(frac=1)で下へ(+AMP)
const span = (EL.length - 1) * CW;
const frac = span > 0 ? Math.max(0, Math.min(1, scene.scrollLeft / span)) : 0.5;
track.style.transform = `translateY(${(AMP * (2 * frac - 1)).toFixed(1)}px)`;
// …中央に来た元素の readout / 電子配置を更新
}
6. 電子配置を構成原理で計算する(+例外 20 元素)
フッターの電子配置図(ボーアモデル風)と 1s² 2s² 2p⁶ … の表記は、構成原理(マーデルングの順)で計算しています。
const ORB = [
["1s", 2], ["2s", 2], ["2p", 6], ["3s", 2], ["3p", 6],
["4s", 2], ["3d", 10], ["4p", 6], ["5s", 2], ["4d", 10],
// …7p まで
];
function config(z) {
const c = {};
ORB.forEach(([k]) => (c[k] = 0));
let e = z;
for (const [k, cap] of ORB) {
const t = Math.min(cap, e);
c[k] = t;
e -= t;
if (e <= 0) break;
}
(EXC[z] || []).forEach(([k, d]) => (c[k] += d)); // 例外元素を補正
return c;
}
ただし構成原理どおりにならない元素があります(Cr, Cu, Nb, Mo, Ru, Rh, Pd, Ag, La, Ce, Gd, Pt, Au, Ac, Th, Pa, U, Np, Cm, Lr の 20 個)。これらだけ EXC テーブルで実測に合わせて補正します。
const EXC = {
24: [["4s", -1], ["3d", 1]], // Cr
29: [["4s", -1], ["3d", 1]], // Cu
46: [["5s", -2], ["4d", 2]], // Pd
// …
};
上付き数字は、当初 Unicode の上付き文字(² ⁶)で描いていましたが、小さくて読みにくかったので <sup> タグに置き換えました。フォント環境に依存せず、拡大時も綺麗です。
7. 静的辞書だけで多言語化する(/ja /en)
日本語/英語対応です。教育コンテンツなので元素名・化学用語の正確性が最優先。外部の翻訳 API に投げると誤訳のリスクがあるので、訳文をアプリ内に静的に持つ方式にしました(オフラインでも動く)。
言語判定は優先順位つき:
function detectLang() {
const q = new URLSearchParams(location.search).get("lang");
if (q === "ja" || q === "en") return q; // ?lang=en
const m = location.pathname.match(/\/(ja|en)\/?$/);
if (m) return m[1]; // /element_belt/en
const saved = localStorage.getItem("eb_lang");
if (saved === "ja" || saved === "en") return saved; // 前回の選択
return (navigator.language || "").toLowerCase().startsWith("en")
? "en" : "ja"; // ブラウザ言語
}
UI 文言は data-i18n-* 属性で宣言し、applyI18n() が一括で流し込みます。表示テキストは残らず関数/属性経由にするのが肝で、ここを一部でもハードコードすると「一部だけ翻訳される」不具合になります(実際にやらかしたことがある)。
function applyI18n() {
document.documentElement.lang = LANG;
document.title = t("title");
document.querySelectorAll("[data-i18n]").forEach((el) => (el.textContent = t(el.dataset.i18n)));
document.querySelectorAll("[data-i18n-html]").forEach((el) => (el.innerHTML = t(el.dataset.i18nHtml)));
document.querySelectorAll("[data-i18n-tip]").forEach((el) => el.setAttribute("data-tip", t(el.dataset.i18nTip)));
document.querySelectorAll("[data-i18n-aria]").forEach((el) => el.setAttribute("aria-label", t(el.dataset.i18nAria)));
}
/element_belt/ja /element_belt/en のパスでの言語切替は、サーバー側(Apache の .htaccess)で両パスを index.html に内部リライトし、JS が location.pathname から言語を読む、という分担にしています。相対パスの資産(favicon)が /en 配下で 404 にならないよう、favicon の href だけは配信ディレクトリ基準で組み立て直しています。
8. アクセシビリティ(デジタル庁デザインシステム参照)
デジタル庁デザインシステムを参考に、次を意識しました。
- 文字サイズ・コントラスト 4.5:1 以上(薄地でも読める副次テキスト色を別に定義)
- キーボード操作(← → で原子番号移動、↑ ↓ で価数列の移動)とフォーカスリング表示
- ツールチップは Tab 到達・Enter/Space 開閉に対応、
aria-describedbyを付与 - 初学者向けの解説を全 118 元素とすべてのラベルに用意
まとめ
- 「族」ではなく「価数」でそろえると、Ca と Cd が同列に来る面白い周期表になる
- d・f ブロックの両端を s+p の価数列に折り込むのが
eleCol()の勘所 - ビルドレス単一 HTML は、教育用の小さなアプリと相性がいい
- 多言語は静的辞書+属性宣言でオフライン・正確・軽量に
触ってみて、縦(同族)方向のつながりを眺めてもらえると嬉しいです。
引用・クレジット
配列は、京都大学の前野悦輝先生が考案された立体周期表「エレメンタッチ(ElemenTouch)」に着想を得た独立の実装です(名称・デザインの流用ではありません)。前野先生のご了承のもとで公開しています。
Y. Maeno, K. Hagino, T. Ishiguro,
"Three related topics on the periodic tables of elements,"
Foundations of Chemistry 23, 201–214 (2021).
https://doi.org/10.1007/s10698-020-09387-z (オープンアクセス)
- エレメンタッチ 公式サイト: https://ss.scphys.kyoto-u.ac.jp/elementouch/
- 本アプリのコード・ドキュメント・デザインは CC BY 4.0(提供: EMUYN LLC / 教育ブランド EDUYN)
- エレメンタッチの配列を用いた商用利用は、エレメンタッチのエージェントへの事前相談が必要です
