PerfectVector
著者: Irene Kim

Cytoscape.jsのSVGノード画像:サイズ調整と余白の直し方

動く例でCytoscape.jsのノードに独自のSVGを収めます。元画像の余白、containとcoverを比較し、読み込みとラベルの配置を確認します。

3 分で読める
目次

ノードのbackground-imageスタイルにSVG画像のURLを指定し、見せたいアートワークに合うフィット方法を選びます。エンブレムが読み込まれても小さく見える場合は、グラフのノードを大きくする前に、SVGのビューポート内の余白を調べてください。両端が消える場合は、画像とノードの縦横比を比較し、クリッピングを確認します。

Cytoscape.jsは、contain、cover、noneのフィット方法を含むSVGのノード背景を文書化しています。このガイドでは、一つのパスで自作したエンブレムを使い、元画像の範囲とグラフのスタイルを切り分けます。編集可能なアートワークのマスターを、グラフ設定と一緒に保管してください。

元画像とグラフのノードを分ける

確認する四角い範囲は、アートワークの描画範囲、SVG画像のビューポート、ノード本体の三つです。SVGのviewBoxは、ビューポートに対応付ける元の座標領域を定義します。見える図形よりはるかに広い領域を含んでいる場合があります。

余白を抑えた元画像は120 × 60で、コーラル色のエンブレムはx=6からx=114、y=6からy=54までです。中央の開口部も同じパスの一部です。

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg>
<svg xmlns="http://www.w3.org/2000/svg"
     width="120" height="60" viewBox="0 0 120 60">
  <path fill="#f36f59" fill-rule="evenodd"
    d="M6 30L30 6H90L114 30L90 54H30Z M50 20H70V40H50Z"/>
</svg>

余白付きの版では、外側のSVG寸法とviewBoxだけを変えます。

<svg xmlns="http://www.w3.org/2000/svg"
     width="200" height="140" viewBox="-40 -40 200 140">

ヘッダー、パス、閉じタグは同じものを使ってください。両ファイルが描くパスの座標は同一です。二つ目はその周囲により多くの余白を持ちます。その余白は画像とともに拡大縮小されるため、背景のフィット方法を変えるだけでは、適切な元画像の範囲を選ぶ代わりにはなりません。ファイルのビューポートと見えるアートワークが合わない場合は、SVGのバウンディングボックスガイドが役立ちます。

四つのケースを持つ例を動かす

Cytoscape.js 3.34.3を、ローカルで配信したバージョン固定の公式ブラウザーバンドルでテストしました。そのリリースタグのバンドルをcytoscape-3.34.3.min.jsとして、以下のexample.htmlと同じ場所に保存します。たとえばpython3 -m http.server 8000でフォルダーをローカルHTTP配信し、http://localhost:8000/example.htmlを開きます。

この例は元のSVGをデータURIにエンコードします。encodeURIComponent()は、塗りの色に含まれる#など、URIの文字をエスケープします。ライブラリーのSVGの案内では、Base64ではなくこのエンコードを使うことと、ここに示すXMLヘッダーを推奨しています。これは外部フォントや画像を使わない、自己完結した一つのエンブレムです。

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>SVG node fit comparison</title>
<style>
  body { font: 16px system-ui; margin: 20px; }
  #cy { width: 1100px; height: 250px; border: 1px solid #ddd; }
  #status { white-space: pre-wrap; }
</style>
<div id="cy"></div>
<pre id="status">Loading images</pre>
<script src="cytoscape-3.34.3.min.js"></script>
<script>
const header = '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg>';
const path = 'M6 30L30 6H90L114 30L90 54H30Z M50 20H70V40H50Z';
function artwork(padded) {
  const width = padded ? 200 : 120;
  const height = padded ? 140 : 60;
  const viewBox = padded ? '-40 -40 200 140' : '0 0 120 60';
  return header + `<svg xmlns="http://www.w3.org/2000/svg"
    width="${width}" height="${height}" viewBox="${viewBox}">
    <path fill="#f36f59" fill-rule="evenodd" d="${path}"/></svg>`;
}
const cases = [
  ['tight contain', false, 'contain'],
  ['tight cover', false, 'cover'],
  ['padded contain', true, 'contain'],
  ['padded cover', true, 'cover']
].map(([label, padded, fit]) => ({
  label, fit,
  image: 'data:image/svg+xml;utf8,' + encodeURIComponent(artwork(padded))
}));
Promise.all(cases.map(item => new Promise((resolve, reject) => {
  const image = new Image();
  image.onload = () => resolve({
    label: item.label,
    width: image.naturalWidth,
    height: image.naturalHeight
  });
  image.onerror = () => reject(new Error(item.label + ' failed to load'));
  image.src = item.image;
}))).then(loaded => {
  cytoscape({
    container: document.getElementById('cy'),
    elements: cases.map((item, index) => ({
      data: { id: 'n' + index, ...item },
      position: { x: 110 + index * 260, y: 95 }
    })),
    style: [{ selector: 'node', style: {
      width: 120, height: 80, padding: 0, shape: 'rectangle',
      'background-color': '#f1f3f5',
      'background-image': 'data(image)',
      'background-fit': node => node.data('fit'),
      'background-width': 'auto', 'background-height': 'auto',
      'background-width-relative-to': 'inner',
      'background-height-relative-to': 'inner',
      'background-position-x': '50%', 'background-position-y': '50%',
      'background-repeat': 'no-repeat', 'background-clip': 'node',
      'border-width': 1, 'border-color': '#222',
      label: 'data(label)', 'text-valign': 'bottom',
      'text-halign': 'center', 'text-margin-y': 10, 'font-size': 12
    }}],
    layout: { name: 'preset', fit: false },
    zoom: 1, pan: { x: 0, y: 0 },
    userZoomingEnabled: false, userPanningEnabled: false,
    boxSelectionEnabled: false
  });
  document.getElementById('status').textContent =
    'Cytoscape ' + cytoscape.version + '\n' +
    JSON.stringify(loaded, null, 2);
}).catch(error => {
  document.getElementById('status').textContent = error.message;
});
</script>
</html>

ステータス欄は、グラフの初期化前に先読みした画像の寸法を報告します。MDNの説明のとおり、naturalWidthは画像本来の密度補正済みの幅をCSSピクセルで取得します。このテストファイルでは、余白を抑えた画像は120 × 60、余白付き画像は200 × 140と報告されました。これは画像の寸法であり、コーラル色のパスの計測値ではありません。

ノードのサイズを固定して違いを比較する

モデル上の四つのノードはすべて120 × 80で、ノードのパディングはゼロです。元のテストファイルをChromeで、グラフのズーム1と1.5で確認しました。ノードのスタイルは120 × 80のままで、報告された描画寸法は1.5で180 × 120になりました。これはそのブラウザー内での静的な比較であり、互換性やズーム性能のベンチマークではありません。

元画像とフィットこのテストファイルで観察した見た目
余白を抑える、containエンブレム全体が見え、尖った両端も見える
余白を抑える、coverエンブレムが大きくなり、左右の先端がノードで切れる
余白付き、contain小さなエンブレムの周囲に多くの余白がある
余白付き、cover余白付きcontainより少し大きいが、余白を抑えたケースよりかなり小さい

余白を抑えた元画像の比率は2:1で、ノードは1.5:1です。120 × 60の画像全体を120 × 80の内側に収めると、縦方向に余白が残ります。そのノードを覆う場合は、クリッピング前に画像を160 × 80に拡大するため、横の両端が本体からはみ出します。

余白付きの元画像では、画像領域に四辺それぞれ40単位の余白が追加されています。containでは、拡大縮小率は120/200ではなく80/140で制限されます。そのため、変わらない幅108単位のエンブレムは、モデル単位で約62を占めます。この数値は宣言した寸法からの計算であり、スクリーンショットのピクセル計測ではありません。

元画像の範囲とフィット
元画像に大きな余白がある小さなエンブレム、ノード枠内に全体が収まる大きなエンブレム、狭い枠で先端が切れたエンブレムのイラスト。
模式図:元画像の余白とフィット方法は、それぞれ独立して見えるアートワークを変えます。この概念図はCytoscapeのスクリーンショットや変換結果ではありません。

アートワークの範囲を確認してからフィット方法を選ぶ

エンブレム全体が重要な場合は、余白を抑えた元画像とcontainから始めます。見せる必要があるストロークや効果のための余白は残してください。ビューポートを詰めすぎると、必要なアートワークまで切れる場合があります。すべての余白を取り除く指示ではありません。

デザイン上、端の切り抜きが許容される場合にcoverを使います。この例では尖った先端を隠してしまうため、その先端がエンブレムの特徴なら不向きです。ノードを横に広げると、フィットの比率が変わって先端が見えるようになる場合がありますが、グラフのレイアウトも変わります。縦横を比例して大きくしても、切り抜き方は変わりません。どの制約を変えるか決めてください。

この比較では、background-widthとbackground-heightをautoのままにします。明示的な値は、フィットに使う画像寸法を上書きすることがあります。ノードのpaddingも別の入力です。背景のサイズ計算に含めるか、内側の本体を使うかは、ライブラリーに説明があります。これらの設定を加える前に、background-imageリファレンスを確認してください。パディングなしのこのテストでは、まず元画像の余白だけを切り分けています。

SVG自体のmeet、slice、配置の判断については、preserveAspectRatioガイドに進んでください。SVG内部のフィット方法を変える操作と、Cytoscapeの背景のフィット方法を変える操作は別です。

クリッピングより先に読み込みを診断する

ステータスに読み込み失敗が表示される場合、次にすることはcontainからcoverへの変更ではありません。画像リソースだけを開き、URLと宣言した寸法を確認してから、上記の自己完結したパスだけのエンブレムまで単純化します。データURIでは、生のマークアップや、エスケープしていない色値を連結せず、SVG文字列全体をエンコードしてください。

画像として使うSVGには、ドキュメントとして開くSVGとは異なる制約があります。MDNの画像としてのSVGガイドは、画像のコンテキストでのスクリプトや外部リソースの制約を説明しています。外部スタイルシートやフォントに依存するアートワークファイルには、別の納品テストが必要です。この例はそうした依存を避け、グラフのラベルをSVGの外に置いています。

画像が読み込まれる場合は、明らかに横長の図形でフィットとノードのクリッピングを確認します。その後、アプリケーションが対応するノードサイズとブラウザー環境で、実際のアートワークをテストしてください。Cytoscapeの文書にはブラウザー固有のSVGの注意点があります。このChromeでの観察で、Firefoxやほかのすべてのエンジンの問題まで解決できるわけではありません。

ラベルと編集可能なアートワークを分ける

この例はグラフのラベルにlabel: 'data(label)'を使い、SVGのテキストは含みません。エンブレムを作り直さずに文言を変えられます。SVGパスは元のマスターに残り、グラフがノードの識別、配置、関係を管理します。

確認したテストファイルはCanvasの描画面に描いていました。そこでSVGの背景が見えても、内部のパスが個別に編集可能なグラフ要素になるわけではありません。開口部、シルエット、色を変える必要がある場合は、マスターを残してください。グラフの書き出しは別の手順で、書き出しツールの選択と納品テストが必要です。このガイドは、書き出したグラフが編集可能なエンブレムのパスを保持すると約束するものではありません。Graphvizのアートワークガイドでは、別の図描画ツールでの画像参照の受け渡しを扱っています。

独自のエンブレムがPNGやJPGでしか存在しない場合は、その元画像をベクターアートワークに変換し、シルエット、開口部、細部を確認してから、SVGの範囲を設定してください。PerfectVectorは編集可能な元の形状を復元する手助けができます。背景のスタイル、ノードのラベル、グラフの書き出しは引き続き別の段階です。すでに適したSVGがある場合は、そのままフィットの確認に進めます。

よくある質問

coverにしてもSVGのノード画像が小さいのはなぜですか? 元画像のビューポートに、パスの周囲の大きな余白が含まれている場合があります。グラフのレイアウトを変える前に、同じノード寸法で、範囲を詰めたコピーと比較してください。

独自のエンブレムにはcontainとcoverのどちらを使うべきですか? エンブレム全体が重要ならcontainを選びます。観察した端の切り抜きがデザイン上許容される場合だけcoverを選び、自分の元画像とノード形状で検証してください。

SVGのノード背景は編集可能なグラフのパスを作りますか? テストしたCanvasの例は、SVGを画像として描画します。パスを編集するためにアートワークのマスターを保管してください。この例では内部の輪郭は、グラフの別のノードやエッジにはなりません。

ラベルをSVG内に入れられますか? この例はラベルをグラフデータに保持し、アートワークにはパスだけを使います。SVGのテキスト、フォント、画像コンテキストの依存関係には、それぞれブラウザーと納品先での確認が必要です。

出典

  1. Cytoscape.js — 背景画像 — 画像URLのスタイル指定、フィット、サイズの上書き、パディングの参照、SVGの注意点。
  2. Cytoscape.js — リリースv3.34.3 — 元のテストファイルで使った固定バージョン。
  3. Cytoscape.js — v3.34.3ブラウザーバンドル — ブラウザーテスト用にローカル配信した、公式のタグ付き実装。
  4. MDN — viewBox — 元の座標領域とビューポートへのマッピング。
  5. MDN — encodeURIComponent — SVGのデータURI部分のエスケープ。
  6. MDN — naturalWidth — パスの範囲とは異なる、読み込んだ画像の寸法。
  7. MDN — 画像としてのSVG — 画像コンテキストの制約と依存関係の確認。

エンブレムの元がラスター画像なら、編集可能なSVGマスターを作り、輪郭と元画像の範囲を確認してから、自分のノードレイアウトでcontainとcoverを比較してください。

ブログのその他の記事

PerfectVector

編集しやすい
よりきれいなSVGから始めましょう

クレジットカード不要