PerfectVector
Autor: Irene Kim

Znaczniki SVG w Matplotlib scatter: sprawdź ścieżkę

Użyj prostej ścieżki SVG jako znacznika Matplotlib scatter. Odtwórz kontrolę środka, orientacji, otworów i rozmiaru w rzeczywistych eksportach SVG i PDF.

10 min czytania
Na tej stronie

Aby użyć SVG jako własnego znacznika Matplotlib scatter, zamień obsługiwaną geometrię na matplotlib.path.Path, wyśrodkuj wokół (0, 0) i przekaż obiekt do scatter(marker=...). Kolor i rozmiar znacznika pochodzą wtedy z wykresu. API znaczników Matplotlib przyjmuje Path; nie przyjmuje dowolnego dokumentu SVG jako nazwy pliku znacznika.

Samouczek używa autorskiego asymetrycznego wielokąta z prostokątnym otworem. Kompletny skrypt poniżej obsługuje celowo mały podzbiór SVG i odrzuca funkcje, których nie zachowa. Uruchomiliśmy go z Matplotlib 3.10.7, sprawdziliśmy zapisane ścieżki SVG i PDF oraz oba wyrenderowane wyniki.

Jeśli chcesz jedną ilustrację obok eksportowanego wykresu, użyj oddzielnego poradnika kompozycji grafiki SVG w Matplotlib. Znacznik scatter powtarza się we współrzędnych danych, więc jego środek, orientacja i rozmiar wymagają innej kontroli.

1. Zacznij od jednego obsługiwanego kształtu

Potrzebujesz Pythona z Matplotlib i NumPy oraz uprawnienia do zapisu folderu wyjściowego. Przykład zawiera własny SVG, więc nie musisz pobierać ikony. Trzy współrzędne wykresu są ilustracyjne, a nie obserwacjami badawczymi.

Parser przyjmuje dokładnie jeden czarno wypełniony path w korzeniu SVG z viewBox. Każdy kontur musi zamykać się przez Z; każda para współrzędnych wymaga jawnego wielkiego M lub L. Obsługuje ścieżkę złożoną z konturem zewnętrznym i otworem o przeciwnych kierunkach przy fill-rule="nonzero".

Odrzuca krzywe, polecenia względne, transformacje, obrysy, CSS, wiele elementów ścieżek, tekst, przycinanie, gradienty i inne kolory. Odrzuca też dodatkowe atrybuty korzenia, takie jak width i height. Ograniczenie pozwala sprawdzić przykład; to nie ogólny importer SVG. Zachowaj nietkniętą kopię grafiki przed przygotowaniem zgodnego wejścia.

Matplotlib Path przechowuje wierzchołki i kody rysowania zamiast pełnego modelu malowania SVG. API Path definiuje użyte operacje ruchu, linii i zamknięcia. Jeśli symbol wymaga wielu kolorów lub efektów, wybierz proces zachowujący te cechy zamiast po cichu je odrzucać dla dopasowania do parsera.

2. Uruchom kompletny przykład znacznika

Zapisz jako scatter_marker.py, a potem uruchom python scatter_marker.py w tym samym folderze. Powstaną marker-output/marker.svg, scatter.svg, scatter.pdf i podgląd PNG. Wygenerowane źródło SVG i wszystkie ustawienia wykresu są tutaj.

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)

Otwórz scatter.svg i scatter.pdf. Powinny pokazać trzy turkusowe wersje tego samego kształtu: prostokątny otwór po lewej i stopień wystający w dół i w prawo. Porównaj najmniejszy znacznik w docelowym rozmiarze oraz na powiększeniu.

Rzeczywiste eksporty SVG i PDF Matplotlib pokazujące autorski asymetryczny znacznik, jego otwór i trzy sprawdzone rozmiary
Autorski zestaw testowy i rzeczywiste zapisane eksporty Matplotlib 3.10.7. Widok PDF jest renderem zapisanego pliku wektorowego; wartości danych są ilustracyjne.

3. Sprawdź środek i odwróć Y jeden raz

Grafika źródłowa zajmuje x=10…90 i y=10…70. Środek prostokąta ograniczającego to (50, 40). Odjęcie środka daje granice −40…40 poziomo i −30…30 pionowo. To centrowanie prostokąta ograniczającego, a nie środek ciężkości lub oszacowanie równowagi wizualnej.

Dolny prawy stopień jest przydatny, bo symetryczny okrąg ukryłby odwrócenie pionowe. W początkowym układzie współrzędnych SVG dodatnie Y biegnie w dół. Skrypt odwraca znak Y jeden raz, aby zachować wyświetlaną orientację źródła na zwykłym wykresie z Y w górę.

Oba kontury otrzymują tę samą transformację. Ich względne kierunki pozostają przeciwne, więc prostokątny otwór pozostaje otworem. Reguła nonzero SVG zależy od kierunku konturu; źródło evenodd jest tutaj jawnie odrzucane zamiast traktowane jako równoważne.

Następnie Matplotlib normalizuje wyśrodkowany Path. W testowanej wersji wynik rozciąga się od −0.5…0.5 w X i −0.375…0.375 w Y. Punkt przekazany do scatter trafia w środek tych granic. Możesz świadomie wybrać inny punkt zakotwiczenia dla szczególnego symbolu, ale nie zakładaj, że lewy górny narożnik SVG jest właściwym zakotwiczeniem danych.

4. Odczytuj rozmiar scatter w punktach kwadratowych

API scatter definiuje s w punktach kwadratowych, gdzie punkt typograficzny ma 1/72 cala. Niezerowa krawędź znacznika może zwiększyć widoczny rozmiar. Przykład ustawia linewidths=0 i edgecolors="none", aby kontrola dotyczyła wypełnionej geometrii.

Dla tego znormalizowanego znacznika szerokość wynosi jedną jednostkę znacznika, a wysokość 0.75 jednostki. Zapisana szerokość to więc sqrt(s) punktów, a wysokość 0.75 * sqrt(s) punktów.

s (punkty kwadratowe)Oczekiwana i zmierzona szerokośćOczekiwana i zmierzona wysokość
40020 pt15 pt
160040 pt30 pt
360060 pt45 pt

To wymiary tego konkretnego znormalizowanego kształtu, a nie jego zamalowana powierzchnia. Inne proporcje, zakotwiczenie lub obrys krawędzi zmieniają relację parametru rozmiaru do widocznego symbolu. Czterokrotne zwiększenie s podwaja wymiary w tym przykładzie.

Sprawdziliśmy rzeczywiste zapisane współrzędne SVG i operacje wypełnionych ścieżek PDF z tolerancją 0.000001 punktu. Wszystkie trzy znaczniki miały oczekiwane granice, wyśrodkowanie w punkcie danych i przekształcone wierzchołki. Zapisany SVG nie zawierał elementów obrazu, a PDF obiektów obrazu.

Wyrenderowaliśmy też PDF w 144 dpi i sprawdziliśmy trzy zadeklarowane miejsca wewnętrzne: otwór był biały, górne prawe wcięcie białe, a dolne prawe przedłużenie turkusowe. Natywny render przeglądarkowy rzeczywistego SVG przeszedł te same kontrole. Weryfikuje to wybraną orientację i otwór; nie oznacza identycznych wygładzonych krawędzi między rendererami ani obsługi dowolnej grafiki SVG.

5. Napraw przyczynę przed regulacją rozmiaru

ObjawKontrola i naprawa
Parser zgłasza nieobsługiwaną funkcjęSprawdź wskazaną cechę. Przygotuj oddzielny zgodny wielokąt lub pełniejszą metodę konwersji i ją przetestuj. Usunięcie atrybutu bez zachowania efektu może zmienić grafikę.
Kształt jest do góry nogamiSprawdź, czy Y odwrócono już przed funkcją. Zastosuj zmianę jeden raz i porównaj asymetryczną cechę.
Symbol leży poza punktem danychSprawdź granice i wybrane zakotwiczenie. Funkcja centruje granice grafiki niezależnie od pustego miejsca w viewBox.
Otwór się wypełniaSprawdź kierunki konturów i regułę wypełnienia. Zachowaj przeciwne kierunki dla tego przykładu nonzero; parser nie konwertuje geometrii evenodd.
Zapisany plik zawiera symbol rastrowySprawdź eksportowane elementy lub zasoby. Sama nazwa SVG lub PDF nie dowodzi wektorowości każdego elementu wykresu.
Małe znaczniki tracą ważne detaleObejrzyj figurę w końcowym rozmiarze. Powiększ znacznik, uprość symbol lub użyj wbudowanego znacznika, jeśli jaśniej komunikuje kategorię.

Kontrolę plików poza tym przykładem opisuje wyszukiwanie rastra wewnątrz SVG. Dla całej figury sprawdź rozmiar czcionki i grubość linii przy rozmiarze publikacji; poprawna ścieżka może nadal być zbyt szczegółowa dla małej legendy lub zatłoczonego panelu.

Przed użyciem własnego znacznika w gotowej figurze sprawdź najmniejszy rozmiar, wygląd legendy, nakładające się punkty i eksportowane SVG/PDF. Zachowaj dane, osie, etykiety i mapowanie skali w procesie tworzenia wykresu. Własna ikona nie zastępuje tych definicji liczbowych.

Gdy raster potrzebuje nowej ścieżki

Jeśli jedynym zachowanym symbolem jest płaski raster, proces diagramu naukowego do SVG PerfectVector może pomóc odzyskać edytowalną grafikę. Porównaj odzyskany kontur i każdy otwór z oryginałem przed adaptacją do procesu znaczników. Zachowaj istniejący czysty SVG jako źródło, jeśli go masz.

Odzyskiwanie wektorów nie odtwarza danych wykresu ani nie gwarantuje zgodności z tym ograniczonym parserem. Zachowaj fotografie i ilustracje tonalne, gdy ich rastrowy wygląd jest częścią figury, i wybierz odpowiednią metodę umieszczania obrazu.

FAQ

Czy mogę przekazać nazwę pliku SVG bezpośrednio do Matplotlib scatter? Nie. API znaczników przyjmuje obsługiwane opisy, w tym Matplotlib Path. Przykład konwertuje ograniczoną ścieżkę SVG do tego obiektu przed wywołaniem scatter.

Dlaczego mój znacznik jest odwrócony? Źródłowy SVG i wykres mogą mieć różne kierunki Y. Przykład odwraca SVG Y raz; sprawdź asymetryczną cechę, aby nie odwrócić dwukrotnie.

Czy s równa się zamalowanej powierzchni mojego SVG? Nie używaj go jako zmierzonej zamalowanej powierzchni. Scatter definiuje s w punktach kwadratowych, a znormalizowany kształt i obrys określają widoczne wymiary. Ten przykład ma szerokości 20, 40 i 60 punktów przy s równym 400, 1600 i 3600.

Czy kod zachowuje każdą funkcję SVG? Nie. Obsługuje jedną czarną ścieżkę nonzero z jawnymi bezwzględnymi M, L i Z oraz zamkniętymi konturami. Nieobsługiwane malowanie, komendy geometrii, transformacje i dodatkowe elementy powodują błąd.

Źródła

  1. API Matplotlib scatter — definiuje parametr rozmiaru, wejście znacznika i wpływ szerokości krawędzi.
  2. API znaczników Matplotlib 3.10.7 — opisuje znaczniki Path i znormalizowane współrzędne testowanej wersji.
  3. API Matplotlib Path — definiuje wierzchołki, kody rysowania i zamknięte kontury.
  4. Układy współrzędnych SVG — określa początkowe kierunki współrzędnych kontroli orientacji.
  5. Reguły wypełnienia SVG — wyjaśnia wpływ kierunku konturu na otwór nonzero.

Dla symbolu wyłącznie rastrowego odzyskaj edytowalną grafikę diagramu, porównaj kontur i otwory ze źródłem oraz przetestuj dostosowaną ścieżkę we własnej figurze przed przekazaniem.

Więcej z bloga

PerfectVector

Zacznij od czystszego SVG,
który łatwiej edytować

Bez karty kredytowej