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

# Matplotlib의 SVG 산점도 마커: 경로 확인하기

> 간단한 SVG 경로를 Matplotlib 산점도 마커로 사용합니다. 실제 SVG·PDF 내보내기에서 중심, 방향, 구멍, 크기를 재현하고 확인합니다.

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

Language: ko
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의 경로를 검사했으며, 렌더링된 두 출력도 확인했습니다.

내보낸 차트 옆에 그림 하나를 배치하려면 별도의 [Matplotlib SVG 아트워크 합성 가이드](https://perfectvector.com/ko/blog/matplotlib-svg-artwork)를 참고하세요. 산점도 마커는 데이터 좌표마다 반복되므로 중심, 방향, 크기를 별도로 확인해야 합니다.

## 1. 지원되는 도형 하나로 시작하기

Matplotlib과 NumPy가 설치된 Python과 출력 디렉터리에 파일을 쓸 권한이 필요합니다. 예제에 원본 SVG가 포함되어 있어 아이콘을 다운로드할 필요는 없습니다. 플롯에 표시하는 세 좌표는 설명용 값이며 연구 관측값이 아닙니다.

파서는 `viewBox`가 있는 SVG 루트 안에 검은색으로 채워진 `path` 하나만 허용합니다. 각 윤곽선은 `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`를 모두 엽니다. 같은 도형의 청록색 마커 세 개가 보여야 합니다. 왼쪽에는 직사각형 구멍이 있고 오른쪽 아래로는 계단 모양 부분이 돌출되어 있습니다. 가장 작은 마커는 확대 화면뿐 아니라 실제 표시할 크기에서도 비교하세요.

![직접 제작한 비대칭 마커와 구멍, 확인한 세 가지 크기를 보여 주는 실제 Matplotlib SVG·PDF 출력](https://perfectvector.com/blog/matplotlib-svg-scatter-marker/scatter-proof.webp)

*Matplotlib 3.10.7에서 사용한 원본 시험 도형과 실제 저장 출력입니다. PDF 화면은 저장된 벡터 파일을 렌더링한 것이며, 데이터 값은 설명용입니다.*

## 3. 중심을 확인하고 Y를 한 번만 뒤집기

원본 아트워크는 `x=10…90`, `y=10…70` 범위를 차지합니다. 경계 상자의 중심은 `(50, 40)`입니다. 그 중심을 빼면 가로 범위는 `−40…40`, 세로 범위는 `−30…30`이 됩니다. 이는 경계 상자를 기준으로 한 가운데 정렬이며, 도심이나 시각적 균형을 추정한 값은 아닙니다.

대칭 원은 세로 반전을 드러내지 못하므로 원본의 오른쪽 아래 계단 모양 부분이 확인에 유용합니다. 초기 [SVG 좌표계](https://www.w3.org/TR/SVG2/coords.html)에서 양의 Y 방향은 아래쪽입니다. 스크립트는 Y의 부호를 한 번 바꿔서, 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 API](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.scatter.html)는 `s`를 포인트 제곱 단위로 정의하며, 타이포그래피 포인트 1은 1/72인치입니다. 두께가 0이 아닌 마커 테두리는 보이는 범위를 키울 수 있습니다. 이 예제는 `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`를 네 배로 늘리면 가로세로 치수가 두 배가 됩니다.

실제로 저장된 SVG 좌표와 PDF의 채워진 경로 연산을 0.000001포인트의 수치 허용 오차로 검사했습니다. 세 마커 모두 예상한 경계, 데이터 위치를 중심으로 한 배치, 변환된 꼭짓점을 가졌습니다. 저장된 SVG에는 이미지 요소가 없었고 PDF에는 이미지 객체가 없었습니다.

저장된 PDF를 144dpi로 렌더링한 뒤 지정한 내부 위치 세 곳도 확인했습니다. 구멍은 흰색, 오른쪽 위의 파인 부분은 흰색, 오른쪽 아래 돌출 부분은 청록색이었습니다. 실제 SVG의 브라우저 기본 렌더링도 같은 내부 확인을 통과했습니다. 이는 선택한 방향과 개구부를 검증한 것이며, 렌더러 간 안티앨리어싱 경계가 동일하거나 임의의 SVG 아트워크를 지원한다는 뜻은 아닙니다.

## 5. 크기를 조절하기 전에 원인 해결하기

| 증상                      | 확인 사항과 복구 방법                                                                                                    |
| ----------------------- | --------------------------------------------------------------------------------------------------------------- |
| 파서에서 지원하지 않는 기능 오류가 발생함 | 오류에 표시된 기능을 검사합니다. 별도의 호환 다각형 입력을 준비하거나, 더 많은 기능을 지원하는 변환 방법을 사용하고 시험합니다. 효과를 보존하지 않고 속성만 제거하면 아트워크가 바뀔 수 있습니다. |
| 도형이 거꾸로 표시됨             | 이 함수에 전달하기 전에 이미 Y가 반전되었는지 확인합니다. 좌표 변경을 한 번 적용한 다음 비대칭 부분을 비교합니다.                                              |
| 기호가 데이터 점에서 떨어져 있음      | 경계와 선택한 기준점을 검사합니다. 이 함수는 `viewBox` 안의 여백과 관계없이 아트워크 경계를 가운데로 옮깁니다.                                             |
| 구멍이 채워짐                 | 윤곽선 방향과 채우기 규칙을 검사합니다. nonzero로 채워진 이 시험 도형에서는 반대 방향을 유지합니다. 파서는 evenodd 도형을 변환하지 않습니다.                         |
| 저장 파일에 래스터 기호가 들어 있음    | 내보낸 요소나 리소스를 검사합니다. 파일 이름이 SVG 또는 PDF라는 사실만으로 플롯의 모든 요소가 벡터라고 입증할 수는 없습니다.                                      |
| 작은 마커에서 중요한 디테일이 사라짐    | 최종 크기에서 그림을 검토합니다. 마커를 키우거나 기호를 단순화하거나, 범주를 더 명확하게 전달할 수 있다면 내장 마커를 사용합니다.                                      |

이 시험 도형을 넘어 파일을 검사하려면 [SVG 내부의 래스터 콘텐츠를 찾는 방법](https://perfectvector.com/ko/blog/svg-contains-raster-image)을 참고하세요. 전체 그림에 대해서는 [출판 크기에서의 글꼴 크기와 선 두께](https://perfectvector.com/ko/blog/scientific-figure-font-size-line-width)를 검토하세요. 마커 경로가 올바르더라도 작은 범례나 복잡한 패널에는 지나치게 정교할 수 있습니다.

완성할 그림에 사용자 지정 마커를 사용하기 전에 가장 작은 크기, 범례 모양, 겹치는 점, 내보낸 SVG/PDF를 확인하세요. 플롯 데이터, 축, 레이블, 척도 매핑은 플로팅 작업 흐름에서 관리합니다. 사용자 지정 아이콘이 이런 수치 정의를 대신하지는 않습니다.

## 래스터 아트워크에 새 경로가 필요할 때

남아 있는 기호가 평면 래스터 그림뿐이라면 PerfectVector의 [과학 도해를 SVG로 변환하는 작업 흐름](https://perfectvector.com/ko/scientific-diagram-to-svg)으로 편집 가능한 아트워크를 복원할 수 있습니다. 마커 파이프라인에 맞추기 전에 복원된 외곽선과 모든 개구부를 원본과 비교하세요. 깨끗한 기존 SVG가 있다면 그것을 원본으로 유지합니다.

벡터 복원은 차트의 기초 데이터를 재구성하지 않으며, 이 제한적인 파서와의 호환성도 보장하지 않습니다. 래스터 모양이 그림의 일부인 사진과 연속 계조 일러스트는 그대로 보존하고, 해당 콘텐츠에 맞는 이미지 기반 배치 방식을 선택하세요.

## 자주 묻는 질문

**SVG 파일 이름을 Matplotlib scatter에 직접 전달할 수 있나요?**
아니요. 마커 API는 Matplotlib Path를 포함한 지원되는 마커 표현을 받습니다. 이 예제는 제한된 SVG 경로를 해당 객체로 변환한 뒤 scatter를 호출합니다.

**사용자 지정 마커가 거꾸로 나오는 이유는 무엇인가요?**
원본 SVG와 플롯의 Y 방향이 다를 수 있습니다. 이 예제는 SVG Y의 부호를 한 번 바꿉니다. 반전을 두 번 적용하지 않도록 비대칭 부분을 검사하세요.

**s는 SVG 도형의 색칠된 면적과 같은가요?**
실측한 색칠 면적 값으로 사용하지 마세요. Scatter는 s를 포인트 제곱으로 정의하지만, 보이는 치수는 정규화된 도형과 테두리 스트로크로 결정됩니다. 이 시험 도형의 너비는 s가 400, 1600, 3600일 때 각각 20, 40, 60포인트입니다.

**이 코드는 SVG의 모든 기능을 보존하나요?**
아니요. 명시적인 절대 M, L, Z 명령과 닫힌 윤곽선을 사용하는 검은색 nonzero 채우기 경로 하나를 지원합니다. 지원하지 않는 페인팅, 도형 명령, 변환, 추가 요소가 있으면 오류가 발생합니다.

## 출처

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/ko/scientific-diagram-to-svg), 외곽선과 구멍을 원본과 비교한 뒤 전달 전에 실제 그림에서 조정한 경로를 시험하세요.

## Explore

- [Read the HTML article](https://perfectvector.com/ko/blog/matplotlib-svg-scatter-marker)
- [LaTeX에서 SVG 사용하기: 도형 유지와 그림 라벨 조판](https://perfectvector.com/ko/blog/svg-in-latex.md)
- [Konva 히트 감지에서 SVG 투명 영역 제외하기](https://perfectvector.com/ko/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)
