PerfectVector
著者: Irene Kim2 分で読める

MatplotlibのSVG:グラフとロゴをパスとして保つ

MatplotlibのグラフをSVGで書き出し、svgutilsで別のロゴパスを合成します。納品ファイルの埋め込みピクセル、配置、フォントの変化を確認します。

目次

Matplotlibの図の中でロゴをベクターのまま保持するには、グラフをSVGで書き出し、ロゴの既存のSVG形状と合成します。OffsetImage でPNGを配置すると、ベクターのグラフ線とラスター画像が混在したSVGになる場合があります。拡張子だけでは、どの部分がパスなのか分かりません。

このガイドでは役割を分けます。Matplotlibがデータからグラフを描き、svgutils がその横に装飾用のバッジを置きます。元の図形がベクターなら、それを使ってください。適切なラスターのイラストしか残っていない場合は、そのイラストを別途復元します。数値を示す図形、軸、ラベルはグラフの元コードに残してください。

簡潔な作業手順

グラフを書き出し、別の図形を合成し、出来上がったSVGを確認して、実際に納品するファイルを開き直します。グラフのスクリプトと図形のマスターを保管し、次のデータ更新時にも図を再生成できるようにしてください。

グラフの元コードに含めるものを選ぶ

ロゴは図形素材で、軸の目盛りはグラフの一部です。両方を1枚の画像として扱うと、後の修正が難しくなります。

図の内容推奨する元データ確認事項
線、マーカー、軸、データラベルMatplotlibのグラフコード値、ラベル、描画方法の選択
既存のベクターロゴやイラストSVGマスターパス、境界、依存関係
写真や顕微鏡画像のパネル元のラスター画像最終サイズでの解像度
ベクター元データを失った単純な装飾モチーフ別途確認した復元候補形の忠実さと不要な領域

一部のパネルが写真なら、図全体をベクターにする必要はありません。その部分はラスターのまま保持します。目的は各要素に適した元データを維持し、組み立てた結果を確認することです。

Matplotlibのsavefigリファレンスでは、出力形式の明示的な選択を説明しています。OffsetImageのAPIは画像コンテナーを説明するもので、SVGパスの読み込み機能ではありません。ファイル単位の診断については、SVG内のラスター画像の見つけ方を参照してください。

納品ファイルに含まれるもの

この例はPython 3.12、Matplotlib 3.10.7、svgutils 0.3.4で実行しました。バッジはオリジナルの2パスの図形です。グラフの4つの値は説明用のデータであり、実験や製品ベンチマークではありません。

比較には両ファイルで同じグラフを使っています。左は32 × 32のPNGバッジを OffsetImage で挿入したもの、右はSVGのパスを、書き出したグラフに合成したものです。拡大部分は実際の納品ファイルから取り出しています。

実際のMatplotlibのSVGファイルと拡大したバッジ部分。左は埋め込んだPNGバッジ、右は合成したSVGバッジのパス
グラフは両ファイルともベクターのままです。この例ではラスター版にimage要素が1つあり、合成版には図形のパスが2つ追加され、image要素はありません。

SVG要素を数えると、次の結果になりました。

ファイルimage要素path要素text要素
グラフのみ0815
PNGバッジ付きグラフ1815
SVGバッジのパス付きグラフ01015

この数は今回の小さな例に対するものです。パス数は品質の点数ではなく、image要素が0でも、別のアプリでSVGのすべての機能が保持されると証明したことにはなりません。ただし、この合成ファイルのバッジが埋め込みPNGではなく形状であることは確認できます。

境界を予測できるグラフを書き出す

プロジェクトの環境に、ここで使ったバージョンをインストールします。

python -m pip install matplotlib==3.10.7 svgutils==0.3.4

次を plot.py として保存し、空の作業フォルダーで実行してください。

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
 
plt.rcParams["svg.fonttype"] = "none"
 
fig = plt.figure(figsize=(6, 3), dpi=72)
ax = fig.add_axes([0.14, 0.22, 0.58, 0.65])
ax.plot([0, 1, 2, 3], [1, 2, 1.5, 3],
        marker="o", color="#173b40")
ax.set(xlabel="Example x", ylabel="Example y",
       title="Illustrative data only")
ax.spines[["top", "right"]].set_visible(False)
fig.savefig("plot.svg", format="svg")
plt.close(fig)

軸は図の一部だけを占め、右側にバッジ用の余白を残しています。この書き出しの viewBox は 0 0 432 216、外寸は 432pt × 216pt です。

外部の図形を配置するときは、この境界を安定させてください。savefigの説明のとおり、bbox_inches="tight" を追加すると保存時の境界が変わります。余白を詰める必要があるなら、書き出した viewBox を確認してから合成座標を更新してください。

svg.fonttype="none" は、この例のラベルをテキストとして保持します。Matplotlibのフォントの説明では、SVGのテキスト出力とパス出力の選択肢を説明しています。テキストのままなら、受け取り側の環境に適切なフォントが必要です。文字の形を固定する必要がある場合は意図的にパス出力を選び、ラベルの編集用にグラフの元コードを保持してください。SVGのフォント変更ガイドでは、そのトレードオフを扱っています。

ラスター化せず図形を合成する

次のオリジナルバッジを badge.svg として、plot.svg と同じ場所に保存します。

<svg xmlns="http://www.w3.org/2000/svg"
     width="100" height="100" viewBox="0 0 100 100">
  <path fill="#173b40" fill-rule="evenodd"
        d="M30 5H70L95 30V70L70 95H30L5 70V30Z
           M34 17L17 34V66L34 83H66L83 66V34L66 17Z"/>
  <path fill="#28bfa3" d="M53 23L30 55H47L43 77L70 43H53Z"/>
</svg>

その後、次の合成スクリプトを実行します。

import svgutils.transform as sg
 
page = sg.SVGFigure("432pt", "216pt")
page.root.set("viewBox", "0 0 432 216")
 
chart = sg.fromfile("plot.svg").getroot()
badge = sg.fromfile("badge.svg").getroot()
badge.moveto(332, 76, scale_x=0.64)
 
page.append([chart, badge])
page.save("vector-logo.svg")

これはsvgutilsの図の合成チュートリアルにある、読み込み、変換、追加、保存の手順に沿っています。ここではsvgutils 0.3.4が対応する引数 scale_x を使いました。古いチュートリアルでは別の拡大縮小の引数を使っています。

100単位のバッジは幅64単位になります。その左上隅はグラフの座標系内の (332, 76) に置かれます。外寸と viewBox を明示することで、この例の配置を曖昧にせず指定できます。これはページ座標で、データ座標ではありません。グラフのデータ点を動かしても、バッジは一緒に動きません。

変化するデータ点に紐づく注釈なら、グラフ座標から書き出したページへの対応を明示的に保つか、その対応関係を提供する合成ツールを使います。固定のページ配置は隅のロゴや独立した説明用パネルに適しています。

SVGマスターにはパス以外の機能も含められます。すでにPNGを埋め込んだSVGは、合成しても混在した内容のままです。より複雑なファイルを組み合わせるときは、参照ID、フォント、外部リンクの素材も確認してください。この例のバッジにはそうした依存関係がありません。

ファイルを確認し、修正をテストする

vector-logo.svg をブラウザーか使用予定の編集ソフトで開きます。バッジ中央の開口部、グラフのラベル、その間の余白を確認してください。拡大した状態だけでなく、実際に使うサイズでも確認します。

小さなXMLチェックなら、プレビューに頼らず構造を確認できます。

import xml.etree.ElementTree as ET
 
root = ET.parse("vector-logo.svg").getroot()
ns = "{http://www.w3.org/2000/svg}"
for tag in ("image", "path", "text"):
    print(tag, len(root.findall(".//" + ns + tag)))

この例では image 0、path 10、text 15 と表示されます。ラスターの比較版はグラフの8パスを保ちながら image 1 と表示されます。その比較版の画像参照にはPNGデータが含まれています。自己完結したラスター内容であり、外部ロゴファイルが見つからない状態ではありません。

次に plot.py の最後の説明用y値を変え、再実行し、合成も再実行してください。グラフは変わり、バッジは確保したページ位置に残るはずです。この修正確認により、一度は正しく見えても古いグラフの書き出しに依存している作業手順を見つけられます。

次の納品工程は別途確認してください。文書に図を入れる場合は、LaTeXでのSVGの手順で書き出しとラベルの選択を説明しています。ブラウザープレビューが成功しただけでは、学術誌の要件への適合や最終PDFでの保持は確認できません。

復元が必要な図形だけを復元する

グラフの元コードがあるなら、そこから書き出してください。ベクター化では、スクリーンショットから数値データやグラフの命令を復元できません。

PerfectVectorが適するのは、単純な装飾用ロゴやイラストがラスター画像でしか残っていない限定的な場合です。科学図版の素材復元の手順を使い、その図形だけに切り抜き、ベクター候補をプレビューして、開口部、細い形状、不要な背景領域を確認します。軸、測定を表す図形、ラベルはトレースに含めないでください。採用した図形SVGをネイティブのグラフと合成し、納品ファイルの確認を繰り返します。

元のベクターマスターがあれば、必ずそれを使ってください。トレースで重要な細部が失われるなら、単純な記号は手で描き直します。写真のパネルはラスターのまま保持します。画像ベクター化の概要では、その方法を選ぶ前に、トレースで何を復元できるかを説明しています。

よくある質問

Matplotlibの図をSVGで保存すると、PNGロゴもベクターになりますか? いいえ。検証したOffsetImageの手順では、グラフはベクター形状として書き出されますが、PNGロゴはSVG内のimage要素のままです。

データ座標を使ってSVGロゴを配置できますか? ここで示した合成は、書き出したページの座標を使います。データ点に紐づくロゴには、明示的な座標の対応関係か、その対応を維持するツールが必要です。

ラベルがパスになったのはなぜですか? MatplotlibのSVGフォント出力設定が、ラベルをテキストとして書き出すか、字形パスとして書き出すかを制御します。書き出す前に設定を選び、後の文言変更用にグラフの元コードを保管してください。

参考資料

  1. Matplotlib — savefig — SVGの明示的な書き出しと、余白を詰めた境界の影響を説明しています。
  2. Matplotlib — OffsetImage — ラスター版の比較で使った画像コンテナーの動作を説明しています。
  3. Matplotlib — Matplotlibのフォント — SVGのテキスト出力と字形パス出力の選択肢を説明しています。
  4. svgutils — 出版品質の図を作る — グラフの独立した書き出し、SVGの合成、変換、組み立てた図の保存を説明しています。

グラフはコードから書き出し、図形素材は別に保持してください。装飾モチーフがラスターしか残っていない場合は、SVG図形の候補を作成して、開口部と境界を確認し、グラフと合成してください。その後、実際の納品ファイルのピクセル、配置、ラベルの変化を確認します。

ブログのその他の記事

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