PerfectVector
Por Irene Kim

Marcadores SVG en Matplotlib: verifica el trazado

Usa un trazado SVG simple como marcador de dispersión en Matplotlib. Reproduce las revisiones de centro, orientación, huecos y tamaño en exportaciones SVG y PDF.

12 min de lectura
En esta página

Para usar un SVG como marcador personalizado en un gráfico de dispersión de Matplotlib, convierte la geometría compatible en un matplotlib.path.Path, céntrala alrededor de (0, 0) y pasa ese objeto a scatter(marker=...). El color y el tamaño del marcador se definen después en el gráfico. La API de marcadores acepta un Path; no recibe un documento SVG arbitrario como nombre de archivo de marcador.

Este tutorial usa un polígono asimétrico original con un hueco rectangular. El script completo de abajo maneja un subconjunto de SVG deliberadamente pequeño y rechaza las funciones que no puede conservar. Lo ejecutamos con Matplotlib 3.10.7, inspeccionamos los trazados SVG y PDF guardados y revisamos ambos resultados renderizados.

Si quieres una ilustración junto a un gráfico exportado, usa la guía aparte de composición de imágenes SVG en Matplotlib. Un marcador de dispersión se repite en las coordenadas de los datos, así que su centro, orientación y tamaño necesitan otra revisión.

1. Empieza con una forma compatible

Necesitas Python con Matplotlib y NumPy disponibles, además de permiso para escribir en un directorio de salida. El ejemplo incluye su SVG original, por lo que no necesitas descargar un ícono. Las tres coordenadas representadas son valores ilustrativos, no observaciones de investigación.

El analizador acepta exactamente un path con relleno negro dentro de una raíz SVG con viewBox. Cada contorno debe cerrarse con Z; cada par de coordenadas necesita un comando explícito M o L en mayúscula. Admite un trazado compuesto con un contorno exterior y un hueco recorridos en sentidos opuestos mediante fill-rule="nonzero".

Rechaza curvas, comandos relativos, transformaciones, trazos, CSS, varios elementos path, texto, recortes, degradados y otros colores. También rechaza atributos adicionales de la raíz, como width y height. La restricción permite inspeccionar el ejemplo; no es un importador SVG general. Conserva una copia intacta de tu diseño antes de preparar una entrada compatible.

Un Path de Matplotlib guarda vértices y códigos de dibujo, no todo el modelo de pintura SVG. La API Path define las operaciones de movimiento, línea y cierre usadas aquí. Si tu símbolo requiere varios colores o efectos, elige un flujo que los conserve en lugar de eliminarlos sin aviso para adaptarlo a este analizador.

2. Ejecuta el ejemplo completo del marcador

Guarda esto como scatter_marker.py y ejecuta python scatter_marker.py en el mismo directorio. Crea marker-output/marker.svg, scatter.svg, scatter.pdf y una vista previa PNG. Aquí se incluyen el SVG fuente generado y toda la configuración del gráfico.

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)

Abre tanto scatter.svg como scatter.pdf. Debes ver tres versiones verde azulado de la misma forma: un hueco rectangular a la izquierda y un escalón que se extiende hacia abajo a la derecha. Compara el marcador más pequeño a su tamaño previsto de visualización, además de ampliarlo.

Exportaciones SVG y PDF reales de Matplotlib con el marcador asimétrico original, su hueco y los tres tamaños revisados
Figura de prueba original y exportaciones reales guardadas con Matplotlib 3.10.7. La vista PDF es un renderizado del archivo vectorial guardado; los datos son ilustrativos.

3. Revisa el centro e invierte Y una sola vez

El diseño fuente ocupa x=10…90 y y=10…70. El centro de su cuadro delimitador es (50, 40). Restar ese centro da límites de −40…40 en horizontal y −30…30 en vertical. Es un centrado por cuadro delimitador, no un centroide ni una estimación del equilibrio visual.

El escalón inferior derecho de la fuente es útil porque un círculo simétrico ocultaría una inversión vertical. En el sistema de coordenadas SVG inicial, Y positiva apunta hacia abajo. El script cambia el signo de Y una vez para conservar la orientación visible de la fuente en el gráfico normal con Y hacia arriba.

Ambos contornos reciben la misma transformación. Sus sentidos relativos siguen siendo opuestos, así que la abertura rectangular se mantiene como hueco. La regla de relleno nonzero de SVG depende de la dirección del contorno; aquí se rechaza explícitamente una fuente evenodd en vez de tratarla como equivalente.

Después, Matplotlib normaliza ese Path centrado. En la versión probada, el resultado abarca −0.5…0.5 en X y −0.375…0.375 en Y. El punto proporcionado a scatter cae en el centro de esos límites. Puedes elegir deliberadamente otro anclaje para un símbolo especializado, pero no supongas que la esquina superior izquierda de un SVG fuente es un anclaje adecuado para los datos.

4. Interpreta el tamaño en puntos al cuadrado

La API scatter define s en puntos al cuadrado; un punto tipográfico equivale a 1/72 de pulgada. Un borde de marcador distinto de cero puede ampliar su extensión visible. El ejemplo establece linewidths=0 y edgecolors="none" para que la revisión de tamaño se centre en la geometría rellena.

Este marcador normalizado tiene una anchura de una unidad de marcador y una altura de 0.75 unidades. Por lo tanto, su anchura guardada es sqrt(s) puntos y su altura es 0.75 * sqrt(s) puntos.

s (puntos al cuadrado)Anchura esperada y medidaAltura esperada y medida
40020 pt15 pt
160040 pt30 pt
360060 pt45 pt

Estas son las dimensiones de esta forma normalizada concreta, no su área pintada. Otra proporción, anclaje o trazo de borde cambia la relación entre el parámetro de tamaño y el símbolo visible. Multiplicar s por cuatro duplica las dimensiones en este ejemplo.

Inspeccionamos las coordenadas SVG realmente guardadas y las operaciones de trazados rellenos del PDF con una tolerancia numérica de 0.000001 puntos. Los tres marcadores tenían los límites esperados, ubicación centrada en los datos y vértices transformados. El SVG guardado no contenía elementos de imagen y el PDF no contenía objetos de imagen.

También renderizamos el PDF guardado a 144 dpi y revisamos tres posiciones interiores declaradas: el hueco era blanco, la muesca superior derecha era blanca y la extensión inferior derecha era verde azulada. El renderizado nativo del SVG real en el navegador pasó las mismas revisiones interiores. Esto verifica la orientación y abertura elegidas; no afirma que los bordes suavizados sean idénticos entre renderizadores ni que se admita cualquier diseño SVG.

5. Corrige la causa antes de ajustar el tamaño

SíntomaRevisión y solución
El analizador genera un error de función no compatibleInspecciona la función indicada. Prepara una entrada poligonal compatible aparte o usa un método de conversión más completo y pruébalo. Eliminar un atributo sin conservar su efecto puede cambiar el diseño.
La forma está de cabezaRevisa si Y ya se invirtió antes de esta función. Aplica el cambio de coordenadas una vez y compara una característica asimétrica.
El símbolo queda lejos del punto de datosInspecciona sus límites y el anclaje elegido. Esta función centra los límites del diseño, sin importar el espacio vacío del viewBox.
El hueco se rellenaInspecciona los sentidos de los contornos y la regla de relleno. Mantén sentidos opuestos para esta figura de prueba con relleno nonzero; el analizador no convierte geometría evenodd.
El archivo guardado contiene un símbolo rasterizadoInspecciona los elementos o recursos exportados. Un nombre de archivo SVG o PDF por sí solo no demuestra que todos los elementos del gráfico sean vectoriales.
Los marcadores pequeños pierden detalles útilesRevisa la figura a su tamaño final. Amplía el marcador, simplifica el símbolo o usa uno integrado si comunica la categoría con más claridad.

Para inspeccionar archivos más allá de esta figura de prueba, consulta cómo encontrar contenido rasterizado dentro de SVG. Para la figura completa, revisa el tamaño de fuente y grosor de línea al tamaño de publicación; un trazado correcto puede seguir siendo demasiado complejo para una leyenda pequeña o un panel saturado.

Antes de usar tu marcador en una figura final, revisa su tamaño más pequeño, su aspecto en la leyenda, los puntos superpuestos y los SVG/PDF exportados. Mantén los datos, ejes, etiquetas y correspondencia de escala en el flujo de creación del gráfico. Un ícono personalizado no sustituye esas definiciones numéricas.

Cuando una imagen rasterizada necesita un trazado nuevo

Si del símbolo solo queda un dibujo rasterizado plano, el flujo de diagramas científicos a SVG de PerfectVector puede ayudar a recuperar una imagen editable. Compara el contorno recuperado y cada abertura con el original antes de adaptarlo al proceso de marcadores. Conserva un SVG limpio existente como fuente cuando ya lo tengas.

La recuperación vectorial no reconstruye los datos subyacentes del gráfico ni garantiza compatibilidad con este analizador limitado. Conserva fotografías e ilustraciones de tono continuo cuando su apariencia rasterizada forme parte de la figura, y elige un método de colocación basado en imágenes adecuado para ese contenido.

Preguntas frecuentes

¿Puedo pasar un nombre de archivo SVG directamente a scatter de Matplotlib? No. La API de marcadores acepta descripciones compatibles, incluido un Path de Matplotlib. Este ejemplo convierte un trazado SVG limitado en ese objeto antes de llamar a scatter.

¿Por qué mi marcador personalizado está de cabeza? El SVG fuente y el gráfico pueden usar distintas direcciones Y. Este ejemplo cambia el signo de Y del SVG una vez; inspecciona una característica asimétrica para evitar invertirla dos veces.

¿s equivale al área pintada de mi forma SVG? No lo uses como medida del área pintada. Scatter define s en puntos al cuadrado, mientras que la forma normalizada y el trazo de borde determinan sus dimensiones visibles. Esta figura de prueba tiene anchuras de 20, 40 y 60 puntos con valores s de 400, 1600 y 3600.

¿Este código conserva todas las funciones de SVG? No. Admite un solo trazado negro con relleno nonzero, comandos absolutos explícitos M, L y Z y contornos cerrados. La pintura, los comandos geométricos, las transformaciones y los elementos adicionales no compatibles generan un error.

Fuentes

  1. API scatter de Matplotlib — Define el parámetro de tamaño, la entrada del marcador y la interacción con el ancho del borde.
  2. API de marcadores de Matplotlib 3.10.7 — Documenta los marcadores Path y sus coordenadas normalizadas para la versión probada.
  3. API Path de Matplotlib — Define vértices, códigos de dibujo y contornos cerrados.
  4. Sistemas de coordenadas SVG — Establece las direcciones iniciales usadas en la revisión de orientación.
  5. Reglas de relleno SVG — Explica cómo la dirección del contorno afecta la abertura con relleno nonzero.

Para un símbolo disponible solo como imagen rasterizada, recupera un diagrama editable, compara su contorno y sus huecos con la fuente y prueba el trazado adaptado en la figura real antes de entregarla.

Más del blog

PerfectVector

Empieza con un SVG más limpio
y fácil de editar

Sin tarjeta de crédito