PerfectVector
Автор: Irene Kim

Маркеры SVG в Matplotlib scatter: проверьте контур

Используйте простой контур SVG как маркер scatter в Matplotlib. Повторите проверки центра, ориентации, отверстий и размера в реальных экспортах SVG и PDF.

10 мин чтения
На этой странице

Чтобы использовать SVG как собственный маркер scatter в Matplotlib, преобразуйте поддерживаемую геометрию в matplotlib.path.Path, отцентрируйте вокруг (0, 0) и передайте объект в scatter(marker=...). Цвет и размер маркера затем задаются графиком. API маркеров Matplotlib принимает Path, а не имя произвольного документа SVG.

В руководстве используется собственный асимметричный многоугольник с прямоугольным отверстием. Полный скрипт ниже обрабатывает намеренно малую часть SVG и отвергает возможности, которые не может сохранить. Мы запускали его с Matplotlib 3.10.7, проверили сохранённые контуры SVG и PDF и обе отрисовки.

Если нужна одна иллюстрация рядом с экспортированным графиком, используйте отдельное руководство по композиции SVG в Matplotlib. Маркер scatter повторяется в координатах данных, поэтому центр, ориентация и размер требуют иной проверки.

1. Начните с одной поддерживаемой фигуры

Нужны Python с доступными Matplotlib и NumPy и право записи выходной папки. Собственный SVG включён в пример, поэтому скачивать значок не нужно. Три нанесённые координаты — иллюстративные значения, а не исследовательские наблюдения.

Парсер принимает ровно один залитый чёрным path внутри корневого SVG с viewBox. Каждое очертание должно замыкаться Z; перед каждой парой координат нужна явная заглавная M или L. Поддерживается составной контур с внешней границей и отверстием противоположного направления при fill-rule="nonzero".

Он отвергает кривые, относительные команды, преобразования, обводки, CSS, несколько элементов path, текст, обтравку, градиенты и другие цвета. Также запрещены дополнительные корневые атрибуты вроде width и height. Ограничение делает пример проверяемым, но не универсальным импортёром SVG. Сохраните нетронутую копию рисунка до подготовки совместимого входа.

Matplotlib Path хранит вершины и коды рисования, а не полную модель окрашивания SVG. Path API определяет использованные операции перемещения, линии и замыкания. Если символу нужны разные цвета или эффекты, выберите сохраняющий их процесс вместо молчаливого удаления ради этого парсера.

2. Запустите полный пример маркера

Сохраните как scatter_marker.py, затем выполните python scatter_marker.py в той же папке. Скрипт создаёт marker-output/marker.svg, scatter.svg, scatter.pdf и предпросмотр PNG. Исходник создаваемого SVG и все настройки графика включены здесь.

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 с исходным асимметричным маркером, его отверстием и тремя проверенными размерами
Собственный образец и реальные сохранённые экспорты Matplotlib 3.10.7. Вид PDF — отрисовка сохранённого векторного файла; значения данных иллюстративны.

3. Проверьте центр и один раз переверните Y

Исходный рисунок занимает x=10…90 и y=10…70. Центр ограничивающей рамки — (50, 40). Вычитание центра даёт границы −40…40 по горизонтали и −30…30 по вертикали. Это центрирование по рамке, а не центроид или оценка визуального равновесия.

Нижняя правая ступенька полезна: симметричный круг скрыл бы вертикальный переворот. В начальной системе координат SVG положительная Y направлена вниз. Скрипт один раз меняет знак Y, сохраняя видимую ориентацию источника на обычном графике с Y вверх.

Оба очертания получают одинаковое преобразование. Их взаимное направление остаётся противоположным, поэтому прямоугольный просвет остаётся отверстием. Правило заливки nonzero SVG зависит от направления; источник evenodd явно отвергается, а не считается эквивалентным.

Затем Matplotlib нормализует отцентрированный Path. В проверенной версии результат занимает −0.5…0.5 по X и −0.375…0.375 по Y. Точка, переданная в scatter, попадает в центр этих границ. Для особого символа можно намеренно выбрать другую точку привязки, но не считайте верхний левый угол SVG подходящей привязкой к данным.

4. Читайте размер scatter в квадратных пунктах

API scatter определяет s в квадратных пунктах; типографский пункт равен 1/72 дюйма. Ненулевая обводка может увеличить видимые границы маркера. В примере заданы linewidths=0 и edgecolors="none", чтобы проверять размер залитой геометрии.

Для этого нормализованного маркера ширина равна одной единице маркера, высота — 0,75 единицы. Поэтому сохранённая ширина равна sqrt(s) пунктов, а высота — 0.75 * sqrt(s) пунктов.

s (квадратные пункты)Ожидаемая и измеренная ширинаОжидаемая и измеренная высота
40020 pt15 pt
1 60040 pt30 pt
3 60060 pt45 pt

Это размеры конкретной нормализованной фигуры, а не её закрашенная площадь. Другое соотношение сторон, привязка или обводка меняют связь параметра размера с видимым символом. Увеличение s в четыре раза удваивает размеры в этом примере.

Мы проверили реальные координаты сохранённого SVG и операции залитых контуров PDF с числовым допуском 0,000001 пункта. Все три маркера имели ожидаемые границы, центрированное размещение и преобразованные вершины. Сохранённый SVG не содержал элементов изображений, а PDF — объектов изображений.

Мы также отрисовали сохранённый PDF при 144 dpi и проверили три заданные внутренние точки: отверстие было белым, верхний правый вырез — белым, нижний правый выступ — бирюзовым. Нативная браузерная отрисовка настоящего SVG прошла те же проверки. Это подтверждает выбранные ориентацию и отверстие, но не утверждает одинаковые сглаженные края разных движков или поддержку произвольных SVG.

5. Исправьте причину до изменения размера

СимптомПроверка и исправление
Парсер сообщает неподдерживаемую возможностьИзучите названную возможность. Подготовьте отдельный совместимый многоугольник либо используйте более полный метод конвертации и испытайте его. Удаление атрибута без сохранения эффекта меняет рисунок.
Фигура перевёрнутаПроверьте, не переворачивалась ли Y до этой функции. Измените координаты один раз, затем сравните асимметричную деталь.
Символ смещён от точки данныхПроверьте границы и привязку. Функция центрирует границы рисунка независимо от пустого места в viewBox.
Отверстие залилосьПроверьте направления очертаний и правило заливки. Для этого образца nonzero сохраняйте противоположные направления; парсер не преобразует evenodd.
В сохранённом файле символ растровыйИзучите экспортированные элементы или ресурсы. Одно расширение SVG или PDF не доказывает векторность всех элементов графика.
Малые маркеры теряют полезные деталиПроверьте рисунок в итоговом размере. Увеличьте маркер, упростите символ или используйте встроенный маркер, если он яснее передаёт категорию.

Для проверки других файлов см. поиск растра внутри SVG. Для всей иллюстрации проверьте размер шрифта и толщину линий в публикационном размере: даже верный контур маркера может быть слишком сложным для маленькой легенды или тесной панели.

До использования своего маркера в готовой иллюстрации проверьте минимальный размер, вид легенды, перекрывающиеся точки и экспорт SVG/PDF. Данные, оси, подписи и отображение масштаба оставляйте в процессе построения графика. Собственный значок не заменяет эти числовые определения.

Когда растровому рисунку нужен новый контур

Если сохранился только плоский растровый символ, процесс научной схемы в SVG PerfectVector поможет восстановить редактируемый рисунок. Сравните очертание и каждое отверстие с оригиналом до адаптации к маркерам. Если чистый SVG уже есть, используйте его как источник.

Восстановление вектора не восстанавливает исходные данные графика и не гарантирует совместимость с ограниченным парсером. Сохраняйте фотографии и полутоновые иллюстрации, если их растровый вид важен, и выбирайте подходящий способ размещения изображения.

Частые вопросы

Можно ли передать имя SVG напрямую в Matplotlib scatter? Нет. API принимает поддерживаемые описания маркеров, в том числе Matplotlib Path. Здесь ограниченный контур SVG преобразуется в такой объект до вызова scatter.

Почему собственный маркер перевёрнут? SVG и график могут использовать разные направления Y. Пример один раз меняет знак SVG Y; проверяйте асимметричную деталь, чтобы не перевернуть дважды.

Равно ли s закрашенной площади моей фигуры SVG? Не используйте его как измеренную закрашенную площадь. Scatter определяет s в квадратных пунктах, а нормализованная фигура и обводка задают видимые размеры. У этого образца ширина равна 20, 40 и 60 пунктам при s 400, 1600 и 3600.

Сохраняет ли код все возможности SVG? Нет. Поддерживается один залитый чёрным контур nonzero с явными абсолютными M, L и Z и замкнутыми очертаниями. Неподдерживаемые окрашивание, команды геометрии, преобразования и дополнительные элементы вызывают ошибку.

Источники

  1. Matplotlib scatter API — Определяет размер, входной маркер и влияние толщины края.
  2. API маркеров Matplotlib 3.10.7 — Описывает маркеры Path и нормализованные координаты проверенной версии.
  3. Matplotlib Path API — Определяет вершины, коды рисования и замкнутые очертания.
  4. Системы координат SVG — Задаёт исходные направления координат для проверки ориентации.
  5. Правила заливки SVG — Объясняет влияние направления контуров на отверстие при nonzero.

Для растрового символа восстановите редактируемую схему, сравните очертание и отверстия с источником и испытайте адаптированный контур в настоящей иллюстрации до передачи.

Ещё из блога

PerfectVector

Начните с более чистого SVG,
который проще редактировать

Без банковской карты