PerfectVector
Von Irene Kim

SVG-Streudiagrammmarker in Matplotlib: Pfad prüfen

Nutze einen einfachen SVG-Pfad als Matplotlib-Streudiagrammmarker. Prüfe Zentrierung, Ausrichtung, Löcher und Größen in tatsächlichen SVG- und PDF-Exporten.

11 Min. Lesezeit
Auf dieser Seite

Um eine SVG als eigenen Matplotlib-Streudiagrammmarker zu verwenden, wandle die unterstützte Geometrie in einen matplotlib.path.Path um, zentriere ihn um (0, 0) und übergib dieses Objekt an scatter(marker=...). Farbe und Größe des Markers stammen dann aus dem Plot. Matplotlibs Marker-API akzeptiert einen Path; sie nimmt kein beliebiges SVG-Dokument als Markerdateinamen entgegen.

Dieses Tutorial verwendet ein eigenes asymmetrisches Polygon mit rechteckigem Loch. Das vollständige Skript unten verarbeitet eine bewusst kleine SVG-Teilmenge und weist Merkmale zurück, die es nicht erhalten kann. Wir führten es mit Matplotlib 3.10.7 aus, prüften die gespeicherten SVG- und PDF-Pfade und kontrollierten beide gerenderten Ausgaben.

Wenn du eine Illustration neben einem exportierten Diagramm platzieren möchtest, nutze die separate Anleitung zur SVG-Grafikkomposition mit Matplotlib. Ein Streudiagrammmarker wiederholt sich an Datenkoordinaten. Sein Mittelpunkt, seine Ausrichtung und Größe erfordern deshalb eine andere Prüfung.

1. Beginne mit einer unterstützten Form

Du brauchst Python mit verfügbarem Matplotlib und NumPy sowie Schreibrechte für ein Ausgabeverzeichnis. Das Beispiel enthält seine eigene SVG; du musst deshalb kein Symbol herunterladen. Die drei dargestellten Koordinaten sind Beispielwerte, keine Forschungsbeobachtungen.

Der Parser akzeptiert genau einen schwarz gefüllten path innerhalb einer SVG-Wurzel mit viewBox. Jede Kontur muss mit Z schließen; jedes Koordinatenpaar benötigt ein ausdrückliches großes M oder L. Er unterstützt einen zusammengesetzten Pfad mit Außenkontur und Loch durch entgegengesetzte Laufrichtungen unter fill-rule="nonzero".

Er weist Kurven, relative Befehle, Transformationen, Konturen, CSS, mehrere Pfadelemente, Text, Beschneidung, Farbverläufe und andere Farben zurück. Auch zusätzliche Wurzelattribute wie width und height werden zurückgewiesen. Diese Einschränkung macht das Beispiel prüfbar; es ist kein allgemeiner SVG-Importer. Behalte vor dem Vorbereiten einer kompatiblen Eingabe eine unveränderte Kopie deiner Grafik.

Ein Matplotlib-Path speichert Eckpunkte und Zeichenbefehle statt des vollständigen SVG-Farbmodells. Die Path-API definiert die hier verwendeten Bewegungs-, Linien- und Schließoperationen. Wenn dein Symbol mehrere Farben oder Effekte benötigt, wähle einen Ablauf, der diese erhält, statt sie zur Anpassung an diesen Parser stillschweigend zu verwerfen.

2. Führe das vollständige Markerbeispiel aus

Speichere dies als scatter_marker.py und führe anschließend python scatter_marker.py im selben Verzeichnis aus. Es erstellt marker-output/marker.svg, scatter.svg, scatter.pdf und eine PNG-Vorschau. Die erzeugte SVG-Quelle und sämtliche Ploteinstellungen sind hier enthalten.

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)

Öffne sowohl scatter.svg als auch scatter.pdf. Du solltest drei türkisgrüne Varianten derselben Form sehen: links ein rechteckiges Loch und eine nach rechts unten reichende Stufe. Vergleiche den kleinsten Marker sowohl in seiner vorgesehenen Darstellungsgröße als auch in der vergrößerten Ansicht.

Tatsächliche Matplotlib-SVG- und PDF-Exporte mit dem eigenen asymmetrischen Marker, seinem Loch und drei geprüften Größen
Eigene Beispieldatei und tatsächliche gespeicherte Exporte aus Matplotlib 3.10.7. Die PDF-Ansicht ist ein Rendering der gespeicherten Vektordatei; die Datenwerte sind illustrativ.

3. Prüfe den Mittelpunkt und kehre Y einmal um

Die Ausgangsgrafik liegt in x=10…90 und y=10…70. Ihr Begrenzungsrahmenmittelpunkt ist (50, 40). Nach dem Abziehen dieses Mittelpunkts reichen die Grenzen horizontal von −40…40 und vertikal von −30…30. Das ist eine Zentrierung des Begrenzungsrahmens, kein Flächenschwerpunkt oder eine Schätzung des visuellen Gleichgewichts.

Die Stufe rechts unten in der Vorlage ist hilfreich, weil ein symmetrischer Kreis eine vertikale Spiegelung verbergen würde. Im anfänglichen SVG-Koordinatensystem zeigt positives Y nach unten. Das Skript negiert Y einmal, damit die angezeigte Ausrichtung der Vorlage im normalen Plot mit nach oben gerichtetem Y erhalten bleibt.

Beide Konturen erhalten dieselbe Transformation. Ihre relativen Laufrichtungen bleiben entgegengesetzt, sodass die rechteckige Öffnung ein Loch bleibt. SVGs Nonzero-Füllregel hängt von der Konturrichtung ab. Eine evenodd-Vorlage wird hier ausdrücklich zurückgewiesen, statt als gleichwertig behandelt zu werden.

Matplotlib normalisiert anschließend diesen zentrierten Path. In der getesteten Version reicht das Ergebnis in X von −0.5…0.5 und in Y von −0.375…0.375. Der an scatter übergebene Punkt liegt in der Mitte dieser Grenzen. Für ein spezielles Symbol kannst du bewusst einen anderen Anker wählen. Gehe aber nicht davon aus, dass die linke obere Ecke einer SVG-Vorlage ein geeigneter Datenanker ist.

4. Lies die Scattergröße in Quadratpunkt

Die scatter-API definiert s in Quadratpunkt, wobei ein typografischer Punkt 1/72 Zoll entspricht. Ein Markerrand mit einer Breite ungleich null kann die sichtbare Ausdehnung vergrößern. Das Beispiel setzt linewidths=0 und edgecolors="none", damit die Größenprüfung die gefüllte Geometrie betrifft.

Bei diesem normalisierten Marker beträgt die Breite eine Markereinheit und die Höhe 0.75 Einheiten. Die gespeicherte Breite ist deshalb sqrt(s) Punkt und die Höhe 0.75 * sqrt(s) Punkt.

s (Quadratpunkt)Erwartete und gemessene BreiteErwartete und gemessene Höhe
40020 pt15 pt
160040 pt30 pt
360060 pt45 pt

Das sind die Maße dieser bestimmten normalisierten Form, nicht ihre bemalte Fläche. Ein anderes Seitenverhältnis, ein anderer Anker oder Randstrich verändert die Beziehung zwischen Größenparameter und sichtbarem Symbol. In diesem Beispiel verdoppelt ein vierfaches s die Abmessungen.

Wir prüften die tatsächlich gespeicherten SVG-Koordinaten und die gefüllten Pfadoperationen der PDF mit einer numerischen Toleranz von 0.000001 Punkt. Alle drei Marker hatten die erwarteten Grenzen, zentrierte Datenplatzierung und transformierte Eckpunkte. Die gespeicherte SVG enthielt keine Bildelemente, die PDF keine Bildobjekte.

Außerdem renderten wir die gespeicherte PDF mit 144 dpi und prüften drei festgelegte innere Positionen: Das Loch war weiß, die Aussparung rechts oben weiß und die Erweiterung rechts unten türkisgrün. Das native Browserrendering der tatsächlichen SVG bestand dieselben Innenprüfungen. Das verifiziert die gewählte Ausrichtung und Öffnung. Es behauptet weder identische geglättete Ränder in verschiedenen Renderern noch Unterstützung beliebiger SVG-Grafiken.

5. Behebe die Ursache vor der Größenanpassung

SymptomPrüfung und Korrektur
Der Parser meldet ein nicht unterstütztes MerkmalPrüfe das genannte Merkmal. Bereite eine separate kompatible Polygoneingabe vor oder verwende eine umfassendere Umwandlungsmethode und teste sie. Ein Attribut zu entfernen, ohne seine Wirkung zu erhalten, kann die Grafik verändern.
Die Form steht auf dem KopfPrüfe, ob Y vor dieser Funktion bereits umgekehrt wurde. Führe die Koordinatenänderung einmal aus und vergleiche anschließend ein asymmetrisches Merkmal.
Das Symbol liegt abseits des DatenpunktsPrüfe seine Grenzen und den gewählten Anker. Diese Funktion zentriert die Grafikgrenzen unabhängig vom Leerraum in der viewBox.
Das Loch füllt sichPrüfe Konturrichtungen und Füllregel. Behalte bei dieser Nonzero-Beispieldatei entgegengesetzte Laufrichtungen; der Parser wandelt Evenodd-Geometrie nicht um.
Die gespeicherte Datei enthält ein RastersymbolPrüfe die exportierten Elemente oder Ressourcen. Ein SVG- oder PDF-Dateiname allein beweist nicht, dass jedes dargestellte Element vektorbasiert ist.
Kleine Marker verlieren nützliche DetailsPrüfe die Abbildung in ihrer Endgröße. Vergrößere den Marker, vereinfache das Symbol oder nutze einen integrierten Marker, wenn er die Kategorie klarer vermittelt.

Zur Dateiprüfung über dieses Beispiel hinaus lies, wie du Rasterinhalte in SVGs findest. Prüfe für die gesamte Abbildung Schriftgröße und Linienbreite bei Publikationsgröße. Ein korrekter Markerpfad kann für eine kleine Legende oder ein dicht belegtes Teilbild immer noch zu komplex sein.

Bevor du deinen eigenen Marker in einer fertigen Abbildung verwendest, prüfe seine kleinste Größe, die Legendenansicht, überlappende Punkte und die exportierte SVG/PDF. Behalte Daten, Achsen, Beschriftungen und Skalenzuordnung in deinem Plotablauf. Ein eigenes Symbol ersetzt diese numerischen Definitionen nicht.

Wenn Rastergrafiken einen neuen Pfad benötigen

Wenn das Symbol nur noch als flächige Rasterzeichnung vorliegt, kann PerfectVectors Ablauf von wissenschaftlicher Grafik zu SVG bearbeitbare Grafiken gewinnen. Vergleiche den gewonnenen Umriss und jede Öffnung mit dem Original, bevor du sie an deine Marker-Pipeline anpasst. Wenn du bereits eine saubere SVG hast, behalte sie als Quelle.

Vektorwiedergewinnung rekonstruiert weder die zugrunde liegenden Diagrammdaten noch garantiert sie Kompatibilität mit diesem eingeschränkten Parser. Behalte Fotografien und Halbtonillustrationen bei, wenn deren Rasterdarstellung Teil der Abbildung ist, und wähle eine für diese Inhalte geeignete bildbasierte Platzierungsmethode.

Häufige Fragen

Kann ich einen SVG-Dateinamen direkt an Matplotlib scatter übergeben? Nein. Die Marker-API akzeptiert unterstützte Markerbeschreibungen, darunter einen Matplotlib-Path. Dieses Beispiel wandelt einen eingeschränkten SVG-Pfad vor dem scatter-Aufruf in dieses Objekt um.

Warum steht mein eigener Marker auf dem Kopf? Die SVG-Vorlage und der Plot verwenden möglicherweise unterschiedliche Y-Richtungen. Dieses Beispiel negiert SVG-Y einmal. Prüfe ein asymmetrisches Merkmal, damit du die Spiegelung nicht zweimal anwendest.

Entspricht s der bemalten Fläche meiner SVG-Form? Verwende es nicht als gemessenen Wert der bemalten Fläche. Scatter definiert s in Quadratpunkt, während normalisierte Form und Randstrich die sichtbaren Maße bestimmen. Dieses Beispiel hat Breiten von 20, 40 und 60 Punkt bei s-Werten von 400, 1600 und 3600.

Erhält dieser Code jedes SVG-Merkmal? Nein. Er unterstützt einen schwarz und mit Nonzero gefüllten Pfad mit ausdrücklichen absoluten M-, L- und Z-Befehlen sowie geschlossenen Konturen. Nicht unterstützter Farbauftrag, Geometriebefehle, Transformationen und zusätzliche Elemente lösen einen Fehler aus.

Quellen

  1. Matplotlib-scatter-API — definiert Größenparameter, Markereingabe und den Einfluss der Randbreite.
  2. Matplotlib-3.10.7-Marker-API — dokumentiert Path-Marker und normalisierte Markerkoordinaten für die getestete Version.
  3. Matplotlib-Path-API — definiert Eckpunkte, Zeichenbefehle und geschlossene Konturen.
  4. SVG-Koordinatensysteme — legt die anfänglichen Koordinatenrichtungen der Ausrichtungsprüfung fest.
  5. SVG-Füllregeln — erklärt, wie die Konturrichtung die mit Nonzero gefüllte Öffnung beeinflusst.

Bei einem reinen Rastersymbol gewinne bearbeitbare Diagrammgrafiken, vergleiche Umriss und Löcher mit der Vorlage und teste den angepassten Pfad vor der Übergabe in deiner tatsächlichen Abbildung.

Mehr aus dem Blog

PerfectVector

Starte mit einer sauberen SVG,
die leichter zu bearbeiten ist

Keine Kreditkarte nötig