Featured image of post ブログで化学構造式を表示する方法

ブログで化学構造式を表示する方法

Hugoで作ったブログにおける話です

使用する技術

  • SMILES
  • RDKit
  • Molfile
  • OpenChemLib(OCL)

手順のチートシート

以下のファイルをダウンロードする。

  • RDKit

https://unpkg.com/@rdkit/rdkit@2023.9.2-1.0.0/Code/MinimalLib/dist/RDKit_minimal.js

https://unpkg.com/@rdkit/rdkit@2023.9.2-1.0.0/Code/MinimalLib/dist/RDKit_minimal.wasm

上記2つのURLをブラウザで検索するだけでダウンロード可能

ダウンロードしたファイルをリポジトリ内に配置する

それぞれ、リポジトリ内のstatic/jsというディレクトリ内に配置する。

static/js/rdkit/RDKit_minimal.js

static/js/rdkit/RDKit_minimal.wasm

以下の設定ファイルを作成する

リポジトリ内にlayouts/partials/head/custom.htmlというファイルを作成する。 内容は以下をコピペ。

1
2
3
4
5
<script src="/js/rdkit/RDKit_minimal.js?v=20250920a"></script>

<script>window.RDKIT_BASE='/js/rdkit/';</script>

<script src="/js/chem-render.js?v=20250920a" defer></script>

v=~はキャッシュ潰しのため適宜変更可。

static/js/chem-render.jsを作成

static/js/chem-render.jsというファイルを作成し、下記のコードをそのままコピペする。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
// ファイル先頭付近に追加
window.MOL_DEBUG = false;

(function () {
  // ---- 画面上に診断を出す ----
  function diag(msg){
    if(!window.MOL_DEBUG) return;          // ← OFFなら何もしない
    let d = document.getElementById('rdkit-diag');
    if(!d){
      d = document.createElement('div');
      d.id='rdkit-diag';
      d.style.cssText='position:fixed;z-index:99999;left:8px;bottom:8px;background:#111;color:#0f0;font:12px/1.4 monospace;padding:6px 8px;border-radius:6px;opacity:.9;max-width:90vw;word-break:break-all';
      document.body.appendChild(d);
    }
    d.textContent = String(msg);
  }

  // ---- ユーティリティ ----
  function W(el,d){ return parseInt(el.getAttribute('data-w')||d,10)||d; }
  function H(el,d){ return parseInt(el.getAttribute('data-h')||d,10)||d; }
  const tick = () => new Promise(r=>setTimeout(r,0));

  // ---- RDKit 初期化(WASMの場所は window.RDKIT_BASE から)----
  async function rdkitReady(){
    if (window.__RDKIT_READY__)   return window.__RDKIT_READY__;
    if (window.__RDKIT_PROMISE__) return await window.__RDKIT_PROMISE__;

    if (typeof initRDKitModule !== 'function'){
      diag('initRDKitModule 未定義:JSが読み込めていません');
      return null;
    }
    var base = (window.RDKIT_BASE || '/js/rdkit/').replace(/\/?$/,'/');
    diag('WASM fetch base = '+base);

    window.__RDKIT_PROMISE__ = initRDKitModule({
      locateFile: f => base + f
    }).then(mod=>{
      window.__RDKIT_READY__ = mod;
      diag('RDKit init: OK');
      return mod;
    }).catch(e=>{
      diag('RDKit init error: '+ e);
      window.__RDKIT_READY__ = null;
      return null;
    });

    return await window.__RDKIT_PROMISE__;
  }

  // ---- 1分子描画(RDKit → SVG)----
  async function drawOne(el){
    try{
      el.textContent = '描画準備中…';

      const RD = await rdkitReady();
      if(!RD){ el.textContent = 'RDKit初期化に失敗しました'; return; }

      const t = el.querySelector('.molblock');
      const smiles = (el.dataset.smiles || '').trim();
      const input  = (t && t.textContent.trim()) ? t.textContent.trim() : smiles;
      if(!input){ el.textContent = '構造データがありません'; return; }

      let mol;
      try{
        mol = RD.get_mol(input);
      }catch(e){
        el.textContent = 'get_mol 例外: ' + e;
        return;
      }
      if(!mol){ el.textContent = '構造の解釈に失敗しました'; return; }

      let svg;
      try{
        svg = mol.get_svg();
      }catch(e){
        el.textContent = 'get_svg 例外: ' + e;
        mol.delete();
        return;
      }
      mol.delete();

      if (!/(<path|<line|<polygon)\b/i.test(svg)){
        el.textContent = '描画結果が空でした';
        return;
      }

      el.innerHTML = svg;
      const s = el.querySelector('svg');
      if (s){
        s.setAttribute('width',  W(el,320));
        s.setAttribute('height', H(el,220));
        s.style.background = '#fff';
      }
    }catch(e){
      el.textContent = '描画に失敗しました: ' + e;
    }
  }

  // ---- 逐次描画(iPhone対策:1つずつ)----
  async function renderAll(){
    const nodes = Array.from(document.querySelectorAll('div.mol'));
    diag('targets: '+nodes.length);
    for(const el of nodes){ await drawOne(el); await tick(); }
  }

  if (document.readyState === 'loading'){
    document.addEventListener('DOMContentLoaded', renderAll);
  } else {
    renderAll();
  }
})();

記事に化学構造式を書く

* オランザピンの例

1
<div class="mol" data-smiles="CN1CCN(CC1)C/2=N/c4ccccc4Nc3sc(C)cc\23" data-w="320" data-h="220"></div> 

w, hは縦横の長さ。化学式はSMILESの記法で記載する。

↓下のように表示される。


それぞれの技術の解説

了解。いまの「OCLを使わない=RDKitだけで描画」版に合わせて、説明をまるっと書き直しました。


それぞれの技術の解説

SMILESとは

SMILES = 構造式を文字で書く記法

  • 原子C O N

  • 結合:省略=単結合、==二重、#=三重

  • 分岐()(例:CC(=O)O

  • 環の開始/終端数字(例:c1ccccc1 は“1”で開閉)

  • 芳香族:小文字(c n など)

  • 電荷:角括弧([NH4+], [O-]

  • 立体(重要)

    • 二重結合の E/Z/\(例:C/C=C\C)。環閉鎖番号の直前/直後に付く場合も(例:オランザピンの ...cc\23)。
    • 四面体中心は @ / @@

例 ・ベンゼン:c1ccccc1 ・アスピリン:CC(=O)OC1=CC=CC=C1C(=O)O ・オランザピン:CN1CCN(CC1)C/2=N/c4ccccc4Nc3sc(C)cc\23 ・ブレクスピプラゾール:O=C5/C=C\c4ccc(OCCCCN3CCN(c1cccc2sccc12)CC3)cc4N5


RDKit(RDKit.js, WASM)とは

SMILESを“正確に”解釈して、そのままSVGに描くブラウザ用ライブラリ(WebAssembly 版)。 RDKit単体

  • get_mol(smiles)SMILESを解析(E/Z・立体情報も保持)
  • get_svg()2DのSVG直接生成(外部画像なし)

まで完結する。 ※必要に応じて get_molblock() で **Molfile(後述)**も取り出せる。


Molfileとは

**Molfile(MDL Molfile)**は「座標付きの分子テキスト」。E/Zや立体情報も保持できる。 RDKitは Molfile入力も可能なので、SMILESの代わりに Molfileを直に貼って描画もできる。

1
2
3
4
5
6
<div class="mol" data-w="320" data-h="220">
  <textarea class="molblock" hidden>
  [ここにMolfileの本文]
  M  END
  </textarea>
</div>

描画全体の流れ

SMILESを書く → RDKitが解釈 → RDKitがSVGを生成 → その場に差し込む

  1. ページの <head> で、ローカル配信の /js/rdkit/RDKit_minimal.js(+ 同ディレクトリの RDKit_minimal.wasm)を読み込む。 取得先は window.RDKIT_BASE='/js/rdkit/'絶対パス固定

  2. chem-render.v2.js が、記事中の <div class="mol" …> を集め、 RDKitの初期化完了を待ってから1件ずつ順番に描画。

  3. 各要素に対して:

    • data-smiles があれば get_mol(SMILES) → get_svg()
    • <textarea.molblock> があれば(Molfile優先) get_mol(Molfile) → get_svg()
  4. 返ってきた SVG文字列をその要素に innerHTML で差し込み、 width/heightdata-w/data-h で指定したサイズに整える。

ポイント

  • 外部画像を一切使わない/SMILESも改変しない
  • 初期化を待って逐次描画するので、iPhone/Safariでも安定。
  • SVGはテキストなので、拡大しても滲まず、CSSで線の太さ・色を調整可能。

記事側の“文法”(あなたが書くのはこれだけ)

SMILESで描く

1
<div class="mol" data-smiles="CN1CCN(CC1)C/2=N/c4ccccc4Nc3sc(C)cc\23" data-w="320" data-h="220"></div>

Molfileで描く(任意)

1
2
3
4
5
6
<div class="mol" data-w="320" data-h="220">
  <textarea class="molblock" hidden>
  [Molfile本文]
  M  END
  </textarea>
</div>
  • data-w / data-h … 表示サイズ(px)。省略時は 320×220。

  • 生HTMLとして置く(コードブロックや引用の中に入れない)。

  • Hugoでショートコード化するなら(例):

    1
    2
    3
    4
    5
    
    {{/* layouts/shortcodes/mol.html */}}
    {{- $s := or (.Get "s") (.Get 0) -}}
    {{- $w := or (.Get "w") (or (.Get 1) "320") -}}
    {{- $h := or (.Get "h") (or (.Get 2) "220") -}}
    <div class="mol" data-smiles="{{ $s }}" data-w="{{ $w }}" data-h="{{ $h }}"></div>
    

    使用例:

    1
    

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。