Adrials
Adrials

Reputation: 75

How to increase the number of vertices?

I need a parametric form for a matplotlib.path.Path. So I used the .vertices attribute, and it works fine except that the number of points given is too low for the use I want. Here is a code to illustrate :

import numpy as np
from matplotlib import pyplot as plt
import matplotlib.patches as mpat

fig, ax = plt.subplots()
ax.set(xlim=(-6, 6), ylim=(-6, 6))


# generate a circular path
circle = mpat.Arc((0, 0), 10, 10, theta1=20, theta2=220, color='green')
path = circle.get_transform().transform_path(circle.get_path()).cleaned().vertices[:-3]  # get_path is not enough because of the transformation, so we apply to the path the same transformation as the circle has got from the identity circle
ax.add_patch(circle)

# plot path vertices
plt.scatter(x=path[:, 0], y=path[:, 1], color='red', s=2)

shape = len(path)

plt.show()

How to increase the number of points (red) to fetch better the path (green)? Or, how can I increase the len of path?

Thanks in advance!

Upvotes: 4

Views: 258

Answers (2)

Reinderien
Reinderien

Reputation: 15243

This annoyed me enough that I wrote a simple alternative implementation. It doesn't rely on Arc at all, and works fine:

import typing
import numpy as np
from matplotlib.patches import PathPatch
from matplotlib.path import Path

if typing.TYPE_CHECKING:
    from matplotlib.transforms import Transform
    from matplotlib.typing import ColourType


def hires_arc(
    centre: tuple[float, float],
    radius: float,
    theta1: float,
    theta2: float,
    n: int = 10,
    colour: typing.Optional['ColourType'] = None,
    transform: typing.Optional['Transform'] = None,
    zorder: int | None = None,
) -> PathPatch:
    """
    matplotlib's Arc patch looks pretty bad; it has a low and non-configurable resolution. This does
    basically the same thing but with controllable resolution.

    :param centre: x, y pair, centre of circle
    :param radius: from centre
    :param theta1: start angle, radians
    :param theta2: end angle, radians
    :param n: number of vertices
    :param colour: passed to the patch
    :param transform: passed to the patch
    :param zorder: passed to the patch
    """
    codes = np.full(shape=n, fill_value=Path.LINETO, dtype=Path.code_type)
    codes[0] = Path.MOVETO

    angles = np.linspace(start=theta1, stop=theta2, num=n)
    vertices = centre + radius*np.stack((
        np.cos(angles), np.sin(angles),
    ), axis=1)

    return PathPatch(
        path=Path(vertices=vertices, codes=codes),
        edgecolor=colour, facecolor='none', zorder=zorder, transform=transform,
    )

Alternatively, transform a path with the built-in Path.arc:

    scale = Affine2D().scale(radius).translate(
        tx=centre[0], ty=centre[1],
    )
    path = Path.arc(
        theta1=theta1, theta2=theta2, n=n,
    ).transformed(scale)
    return PathPatch(
        path=path, edgecolor=colour, facecolor='none', zorder=zorder, transform=transform,
    )

Upvotes: 1

Matt Pitkin
Matt Pitkin

Reputation: 6417

It's not ideal, but you can just loop over many shorter arcs, e.g.,:

import numpy as np
from matplotlib import pyplot as plt
import matplotlib.patches as mpat

fig, ax = plt.subplots()
ax.set(xlim=(-6, 6), ylim=(-6, 6))


# generate a circular path
dr = 30  # change this to control the number of points
path = np.empty((0, 2))

for i in range(20, 221, dr):
    circle = mpat.Arc((0, 0), 10, 10, theta1=i, theta2=(i + dr), color='green')
    tmppath = circle.get_transform().transform_path(circle.get_path()).cleaned().vertices[:-1]
    path = np.vstack((path, tmppath[:, 0:2]))

    ax.add_patch(circle)

# plot path vertices
plt.scatter(x=path[:, 0], y=path[:, 1], color='red', s=2)

plt.show()

Update

Another option is to hack the Arc class, so that during initialisation, the Path.arc takes in the n keyword. E.g.,

from matplotlib.patches import Path, Arc

class NewArc(Arc):
    def __init__(self, xy, width, height, angle=0.0, theta1=0.0, theta2=360.0, n=30, **kwargs):
        """
        An Arc class with the n keyword argument
        """
    
        fill = kwargs.setdefault('fill', False)
        if fill:
            raise ValueError("Arc objects can not be filled")

        super().__init__(xy, width, height, angle=angle, **kwargs)

        self.theta1 = theta1
        self.theta2 = theta2
        (self._theta1, self._theta2, self._stretched_width,
         self._stretched_height) = self._theta_stretch()

        # add in the n keyword here
        self._path = Path.arc(self._theta1, self._theta2, n=n)

The original code (without the for loop) can then be used and the number of points increased, e.g.,

from matplotlib import pyplot as plt

fig, ax = plt.subplots()
ax.set(xlim=(-6, 6), ylim=(-6, 6))

# generate a circular path using NewArc and the n keyword
circle = NewArc((0, 0), 10, 10, theta1=20, theta2=220, n=40, color='green')
path = circle.get_transform().transform_path(circle.get_path()).cleaned().vertices[:-3]
ax.add_patch(circle)

# plot path vertices
ax.scatter(x=path[:, 0], y=path[:, 1], color='red', s=2)

fig.show()

Upvotes: 1

Related Questions