> [Site index](https://perfectvector.com/llms.txt) — available pages and descriptions.

# MatplotlibのSVG散布図マーカー：パスを確認する

> 単純なSVGパスをMatplotlibの散布図マーカーに使います。実際のSVGとPDFの書き出しで、中心、向き、穴、サイズの確認を再現します。

Source: https://perfectvector.com/ja/blog/matplotlib-svg-scatter-marker

Language: ja
Author: Irene Kim
Published: 2026-10-11T08:00:00+09:00
Updated: 2026-10-11T08:00:00+09:00

SVGを独自のMatplotlib散布図マーカーとして使うには、対応する形状を `matplotlib.path.Path` に変換し、`(0, 0)` を中心に置き、そのオブジェクトを `scatter(marker=...)` に渡します。マーカーの色とサイズはグラフ側から指定します。Matplotlibの[マーカーAPIはPathを受け付けます](https://matplotlib.org/3.10.7/api/markers_api.html)が、任意のSVG文書をマーカーファイル名として受け取るわけではありません。

このチュートリアルは、長方形の穴を持つ独自の非対称な多角形を使います。以下の完全なスクリプトは、意図的に小さく限定したSVGの一部の機能だけを扱い、保持できない機能を拒否します。Matplotlib 3.10.7で実行し、保存したSVGとPDFのパスを調べ、両方の描画を確認しました。

書き出したグラフの隣にイラストを1つ置きたい場合は、別の[MatplotlibでSVGデザインを合成するガイド](https://perfectvector.com/ja/blog/matplotlib-svg-artwork)を使います。散布図マーカーはデータ座標に繰り返し置くため、中心、向き、サイズを別の方法で確認する必要があります。

## 1. 対応する1つの形から始める

MatplotlibとNumPyを使えるPythonと、出力ディレクトリへの書き込み権限が必要です。例には独自のSVGが含まれるため、アイコンをダウンロードする必要はありません。プロットする3つの座標は説明用の値であり、研究の観測値ではありません。

パーサーは、`viewBox` を持つSVGルートの中にある、黒で塗った `path` 1つだけを受け付けます。各輪郭は `Z` で閉じ、座標の組ごとに大文字の `M` または `L` を明示する必要があります。`fill-rule="nonzero"` の下で逆向きの輪郭を使い、外周と穴を持つ複合パスに対応します。

曲線、相対コマンド、変換、ストローク、CSS、複数のパス要素、テキスト、クリップ、グラデーション、他の色は拒否します。`width` や `height` のような追加のルート属性も拒否します。この制限によって例を確認しやすくしていますが、汎用SVGインポーターではありません。対応する入力を準備する前に、変更していないデザインのコピーを残します。

MatplotlibのPathは、SVGの描画モデル全体ではなく、頂点と描画コードを保存します。[Path API](https://matplotlib.org/stable/api/path_api.html)は、ここで使う移動、直線、閉じる操作を定義しています。複数の色や効果が必要なシンボルなら、このパーサーに合わせて黙って削除するのではなく、その機能を保持できる手順を選びます。

## 2. 完全なマーカー例を実行する

これを `scatter_marker.py` として保存し、同じディレクトリで `python scatter_marker.py` を実行します。`marker-output/marker.svg`、`scatter.svg`、`scatter.pdf` とPNGプレビューを作ります。生成するSVGソースとグラフの全設定は、ここに含まれています。

```python
from pathlib import Path as FilePath
import re
import xml.etree.ElementTree as ET

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.path import Path
import numpy as np

SVG = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 80">
  <path fill="#000000" fill-rule="nonzero"
        d="M10 10 L70 10 L70 30 L90 30 L90 70 L10 70 Z
           M25 25 L25 55 L45 55 L45 25 Z"/>
</svg>'''


def svg_marker(svg_text):
    """One black, nonzero-filled path; explicit absolute M/L/Z only."""
    namespace = "{http://www.w3.org/2000/svg}"
    root = ET.fromstring(svg_text)
    if root.tag != namespace + "svg" or set(root.attrib) != {"viewBox"}:
        raise ValueError("Use only an SVG root with a viewBox")
    view = np.array([float(value) for value in root.attrib["viewBox"].split()])
    if view.shape != (4,) or not np.isfinite(view).all() or (view[2:] <= 0).any():
        raise ValueError("Invalid viewBox")
    if len(root) != 1 or root[0].tag != namespace + "path":
        raise ValueError("Use exactly one path, with no groups or other elements")
    node = root[0]
    if any((text or "").strip() for text in [root.text, node.text, node.tail]):
        raise ValueError("Text content is not supported")
    if set(node.attrib) - {"d", "fill", "fill-rule"} or len(node):
        raise ValueError("Unsupported path attribute or child element")
    if node.get("fill", "black") not in {"black", "#000", "#000000"}:
        raise ValueError("Use one solid black fill")
    if node.get("fill-rule", "nonzero") != "nonzero":
        raise ValueError("Use nonzero winding; evenodd is not converted here")
    data = node.get("d", "")
    pattern = r"[MLZ]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?"
    tokens = re.findall(pattern, data)
    if re.sub(pattern, "", data).strip(" ,\n\r\t"):
        raise ValueError("Only explicit absolute M, L and Z commands are supported")
    vertices, codes = [], []
    index, start, open_contour, lines = 0, None, False, 0
    while index < len(tokens):
        command = tokens[index]
        if command in {"M", "L"}:
            if index + 2 >= len(tokens) or tokens[index + 1] in {"M", "L", "Z"} or tokens[index + 2] in {"M", "L", "Z"}:
                raise ValueError("Each M/L needs one explicit coordinate pair")
            point = tuple(float(value) for value in tokens[index + 1:index + 3])
            if not np.isfinite(point).all():
                raise ValueError("Coordinates must be finite")
            if command == "M":
                if open_contour:
                    raise ValueError("Close each contour with Z")
                start, open_contour, lines = point, True, 0
                codes.append(Path.MOVETO)
            else:
                if not open_contour:
                    raise ValueError("Start each contour with M")
                lines += 1
                codes.append(Path.LINETO)
            vertices.append(point)
            index += 3
        elif command == "Z":
            if not open_contour or lines < 2:
                raise ValueError("A closed contour needs at least three vertices")
            vertices.append(start)
            codes.append(Path.CLOSEPOLY)
            open_contour = False
            index += 1
        else:
            raise ValueError("Repeat M/L explicitly before coordinate pairs")
    if open_contour or not vertices:
        raise ValueError("Supply nonempty closed contours")
    path = Path(np.array(vertices), codes)
    box = path.get_extents()
    if box.width <= 0 or box.height <= 0:
        raise ValueError("Path needs nonzero width and height")
    if box.x0 < view[0] or box.y0 < view[1] or box.x1 > view[0] + view[2] or box.y1 > view[1] + view[3]:
        raise ValueError("Path outside viewBox: resolve clipping before import")
    center = np.array([(box.x0 + box.x1) / 2, (box.y0 + box.y1) / 2])
    centered = (path.vertices - center) * np.array([1, -1])
    return Path(centered, path.codes)


if __name__ == "__main__":
    output = FilePath("marker-output")
    output.mkdir(exist_ok=True)
    (output / "marker.svg").write_text(SVG)
    marker = svg_marker(SVG)
    plt.rcParams.update({"svg.fonttype": "none", "pdf.fonttype": 42,
                         "font.family": "DejaVu Sans", "font.size": 11})
    fig, ax = plt.subplots(figsize=(8, 3), dpi=144)
    fig.subplots_adjust(left=.1, right=.96, bottom=.24, top=.8)
    ax.scatter([1, 2, 3], [1.3, 2.4, 1.6], s=[400, 1600, 3600],
               marker=marker, color="#2f6977", linewidths=0, edgecolors="none")
    ax.set(xlim=(0, 4), ylim=(0, 4), xlabel="Illustrative x", ylabel="Illustrative y",
           title="Original SVG marker: s = 400, 1600, 3600 points squared")
    ax.spines[["top", "right"]].set_visible(False)
    fig.savefig(output / "scatter.svg", format="svg")
    fig.savefig(output / "scatter.pdf", format="pdf")
    fig.savefig(output / "scatter.png", format="png")
    plt.close(fig)
```

`scatter.svg` と `scatter.pdf` の両方を開きます。同じ形が青緑で3つ表示されるはずです。左には長方形の穴があり、右下には段状の張り出しがあります。最も小さいマーカーは拡大表示だけでなく、予定する表示サイズでも比べます。

![独自の非対称マーカー、その穴、確認した3サイズを示す実際のMatplotlib SVGとPDFの書き出し](https://perfectvector.com/blog/matplotlib-svg-scatter-marker/scatter-proof.webp)

*独自のサンプルと、Matplotlib 3.10.7から実際に保存した書き出しです。PDF表示は保存したベクターファイルの描画であり、データ値は説明用です。*

## 3. 中心を確認し、Yを1回だけ反転する

元のデザインの範囲は `x=10…90` と `y=10…70` です。バウンディングボックスの中心は `(50, 40)` です。この中心を引くと、横は `−40…40`、縦は `−30…30` の範囲になります。これは外接矩形による中心合わせであり、図心や見た目のバランスの推定ではありません。

元デザインの右下の段差は、対称な円では分からない上下反転を確認するのに役立ちます。初期の[SVG座標系](https://www.w3.org/TR/SVG2/coords.html)では、正のYは下へ向かいます。スクリプトはYの符号を1回だけ反転し、通常のYが上へ向かうグラフで、元画像の表示上の向きを保ちます。

両方の輪郭に同じ変換を適用します。相対的な周回方向は逆のままなので、長方形の穴が残ります。SVGの[nonzero塗り規則](https://www.w3.org/TR/SVG2/painting.html#FillRuleProperty)は輪郭の向きに依存します。ここでは `evenodd` の元データを同等に扱わず、明示的に拒否します。

その後Matplotlibは、中心合わせしたPathを正規化します。検証した版では、結果の範囲はXが `−0.5…0.5`、Yが `−0.375…0.375` です。`scatter` に渡した点は、この範囲の中心に置かれます。特殊なシンボルに別の基準点を意図的に選ぶことはできますが、元SVGの左上隅が適切なデータの基準点だとは考えないでください。

## 4. scatterのサイズを平方ポイントで読む

[scatter API](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.scatter.html)は `s` を平方ポイントで定義し、組版の1ポイントは1/72インチです。マーカーの縁幅がゼロでないと、見える範囲が広がる場合があります。例では `linewidths=0` と `edgecolors="none"` を指定し、塗り形状だけのサイズを確認します。

この正規化したマーカーの幅は1マーカー単位、高さは0.75単位です。そのため、保存した幅は `sqrt(s)` ポイント、高さは `0.75 * sqrt(s)` ポイントです。

| `s`（平方ポイント） | 予想と実測の幅 | 予想と実測の高さ |
| ----------- | ------- | -------- |
| 400         | 20 pt   | 15 pt    |
| 1,600       | 40 pt   | 30 pt    |
| 3,600       | 60 pt   | 45 pt    |

これはこの特定の正規化形状の寸法であり、塗った面積ではありません。縦横比、基準点、縁のストロークが変わると、サイズパラメーターと見えるシンボルの関係も変わります。この例では `s` を4倍にすると寸法が2倍になります。

実際に保存したSVG座標と、PDFの塗り付きパス操作を、0.000001ポイントの数値許容差で調べました。3つのマーカーすべてで、範囲、データ位置への中心合わせ、変換した頂点は予想どおりでした。保存したSVGに画像要素はなく、PDFに画像オブジェクトはありませんでした。

また、保存したPDFを144 dpiで描画し、指定した内部の3点を確認しました。穴は白、右上の切り欠きは白、右下の張り出しは青緑でした。実際のSVGのネイティブなブラウザー描画でも、同じ内部確認を通過しました。これは選んだ向きと穴を検証するもので、描画エンジン間でアンチエイリアスの縁が同一であることや、任意のSVGデザインへの対応を主張するものではありません。

## 5. サイズを調整する前に原因を直す

| 症状                  | 確認と修正                                                                         |
| ------------------- | ----------------------------------------------------------------------------- |
| パーサーが非対応機能のエラーを出す   | 指摘された機能を調べる。対応する多角形入力を別に準備するか、より完全な変換方法を使って検証する。効果を保たず属性だけを消すと、デザインが変わる場合がある。 |
| 形が上下逆になる            | この関数より前にYを反転していないか確認する。座標変換を1回だけ適用し、非対称な特徴を比べる。                               |
| シンボルがデータ点から離れている    | 範囲と選んだ基準点を調べる。この関数は `viewBox` 内の空白に関係なく、デザインの範囲を中心合わせする。                      |
| 穴が埋まる               | 輪郭の向きと塗り規則を確認する。このnonzeroのサンプルでは逆向きを保つ。パーサーはevenodd形状を変換しない。                  |
| 保存ファイルにラスターのシンボルがある | 書き出した要素やリソースを調べる。ファイル名がSVGやPDFというだけでは、すべての描画要素がベクターとは限らない。                    |
| 小さいマーカーで重要な細部が消える   | 仕上がりサイズで図を見る。マーカーを大きくする、シンボルを簡略化する、分類がより明確に伝わるなら組み込みマーカーを使う。                  |

この例以外のファイル確認には、[SVG内のラスター内容の探し方](https://perfectvector.com/ja/blog/svg-contains-raster-image)を参照してください。図全体では、[出版サイズでの文字サイズと線幅](https://perfectvector.com/ja/blog/scientific-figure-font-size-line-width)も確認します。パスが正しいマーカーでも、小さな凡例や混み合ったパネルには複雑すぎる場合があります。

完成図で自分のマーカーを使う前に、最小サイズ、凡例の見た目、点の重なり、書き出したSVGとPDFを確認します。データ、軸、ラベル、縮尺の対応はグラフ作成の工程に残します。独自のアイコンは、これらの数値定義の代わりにはなりません。

## ラスターのデザインに新しいパスが必要な場合

シンボルがフラットなラスターの絵にしか残っていないなら、PerfectVectorの[科学図からSVGへの変換手順](https://perfectvector.com/ja/scientific-diagram-to-svg)で編集可能なデザインを取り出せます。マーカーの処理に合わせる前に、取り出した輪郭とすべての穴を原本と比べます。きれいなSVGがすでにあれば、それを元データとして保ちます。

ベクターの取り出しは、グラフの基になるデータを再構成したり、この制限のあるパーサーとの互換性を保証したりしません。写真や連続階調のイラストは、そのラスターの見た目が図の一部であるならそのまま保ち、内容に合う画像ベースの配置方法を選びます。

## よくある質問

**MatplotlibのscatterにSVGファイル名を直接渡せますか？**
いいえ。マーカーAPIはMatplotlibのPathなど、対応するマーカー記述を受け付けます。この例ではscatterを呼ぶ前に、制限したSVGパスをそのオブジェクトへ変換します。

**独自のマーカーが上下逆になるのはなぜですか？**
元のSVGとグラフでY方向が異なる場合があります。この例はSVGのYの符号を1回反転します。二重反転を避けるため、非対称な特徴を調べてください。

**sはSVG形状の塗った面積に等しいですか？**
塗った面積の実測値として使わないでください。Scatterはsを平方ポイントで定義し、正規化した形と縁のストロークが見える寸法を決めます。この例はsが400、1600、3600のとき、幅が20、40、60ポイントになります。

**このコードはSVGのすべての機能を保持しますか？**
いいえ。黒いnonzeroの塗り付きパス1つ、明示した絶対座標のM、L、Zコマンド、閉じた輪郭に対応します。非対応の描画、形状コマンド、変換、追加要素はエラーになります。

## 出典

1. [Matplotlib scatter API](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.scatter.html) — サイズパラメーター、マーカー入力、縁幅の影響を定義。
2. [Matplotlib 3.10.7のマーカーAPI](https://matplotlib.org/3.10.7/api/markers_api.html) — 検証した版のPathマーカーと正規化マーカー座標を説明。
3. [Matplotlib Path API](https://matplotlib.org/stable/api/path_api.html) — 頂点、描画コード、閉じた輪郭を定義。
4. [SVGの座標系](https://www.w3.org/TR/SVG2/coords.html) — 向きの確認で使った初期座標方向を規定。
5. [SVGの塗り規則](https://www.w3.org/TR/SVG2/painting.html#FillRuleProperty) — 輪郭方向がnonzeroでの穴にどう影響するか説明。

ラスターしかないシンボルなら、[編集可能な図のデータを取り出し](https://perfectvector.com/ja/scientific-diagram-to-svg)、輪郭と穴を元画像と比べ、実際の図で調整したパスを試してから納品してください。

## Explore

- [Read the HTML article](https://perfectvector.com/ja/blog/matplotlib-svg-scatter-marker)
- [LaTeXでSVGを使う：形を保持し図のラベルを組版する](https://perfectvector.com/ja/blog/svg-in-latex.md)
- [Konvaのヒット判定でSVGの透明領域を無視する](https://perfectvector.com/ja/blog/konva-alpha-hit-region.md)
- [Product facts](https://perfectvector.com/product-facts.md)
- [FAQ](https://perfectvector.com/faq.md)
- [Pricing and credits](https://perfectvector.com/pricing.md)
