pythonmatplotlibpatchhatch

How do I change the tiling of hatch applied to a patch in matplotlib?


How do I change the way that matplotlib tiles custom hatching in a patch? I would like to tile the hatch in a grid pattern (like you would square tile) as opposed to the alternating row (like you would lay brick).

┌───┐┌───┐┌───┐
│   ││   ││   │
└───┘└───┘└───┘
┌───┐┌───┐┌───┐
│   ││   ││   │
└───┘└───┘└───┘

vs

┌───┐┌───┐┌───┐
│   ││   ││   │
└───┘└───┘└───┘
──┐┌───┐┌───┐┌─
  ││   ││   ││
──┘└───┘└───┘└─

Here's sample code that generates a simple custom hatch in matplotlib:

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.hatch

square_path = patches.Polygon(
    [[-0.4, -0.4], [4.0, -0.4], [0.4, 0.4], [-0.4, 0.4]],
    closed=True, fill=False).get_path()

class SquareHatch(matplotlib.hatch.Shapes):
    """Custom hatches defined by a path drawn inside [-0.5, 0.5] square.

    Identifier 's'.
    """
    filled = True
    size = 1.0
    path = square_path

    def __init__(self, hatch, density):
        """Initialize the custom hatch."""
        self.num_rows = int((hatch.count('s')) * density)
        self.shape_vertices = self.path.vertices
        self.shape_codes = self.path.codes
        matplotlib.hatch.Shapes.__init__(self, hatch, density)

matplotlib.hatch._hatch_types.append(SquareHatch)

# Create a figure and axis
self.fig, self.ax = plt.subplots()
# Create a square patch with hatching
square = patches.Rectangle(xy=(0.25, 0.25), width=0.5, height=0.5, hatch='s', fill=False)
# Add the square patch to the axis
self.ax.add_patch(square)
# Display the plot
plt.show()

I've been searching and I would think this would be an option, but I'm not finding it. AI said it was possible but gave me an option that didn't work... I'd altar my tile, but I can't work out a way to make my pattern work with an offset 2nd row.


Solution

  • The issue comes from the set_vertices_and_codes function in the Shapes superclass:

    def set_vertices_and_codes(self, vertices, codes):
        offset = 1.0 / self.num_rows
        shape_vertices = self.shape_vertices * offset * self.size
        shape_codes = self.shape_codes
        if not self.filled:
            shape_vertices = np.concatenate(  # Forward, then backward.
                [shape_vertices, shape_vertices[::-1] * 0.9])
            shape_codes = np.concatenate([shape_codes, shape_codes])
        vertices_parts = []
        codes_parts = []
        for row in range(self.num_rows + 1):
            ####### Offsetting occurs here #############
            if row % 2 == 0:
                cols = np.linspace(0, 1, self.num_rows + 1)
            else:
                cols = np.linspace(offset / 2, 1 - offset / 2, self.num_rows)
            ####### Offsetting occurs here #############
            row_pos = row * offset
            for col_pos in cols:
                vertices_parts.append(shape_vertices + [col_pos, row_pos])
                codes_parts.append(shape_codes)
        np.concatenate(vertices_parts, out=vertices)
        np.concatenate(codes_parts, out=codes)
    

    Changing the cols to np.linspace(0, 1, self.num_rows+1) regardless of row number removes the offset, but now the self.num_shapes set earlier in the __init__ of the Shape class is off. Ultimately, I found it easier for the custom hatch to just copy-paste the Shape class with some minor edits to remove the offsetting.

    class SquareHatch(matplotlib.hatch.HatchPatternBase):
        """Custom hatches defined by a path drawn inside [-0.5, 0.5] square.
    
        Identifier 's'.
        """
        filled = True
        size = 1.0
        path = square_path
    
        def __init__(self, hatch, density):
            """Initialize the custom hatch."""
            self.num_rows = int((hatch.count('s')) * density)
            self.shape_vertices = self.path.vertices
            self.shape_codes = self.path.codes
    
            self.num_shapes = (self.num_rows+1)**2
            self.num_vertices = (self.num_shapes *
                                     len(self.shape_vertices) *
                                     (1 if self.filled else 2))
    
        def set_vertices_and_codes(self, vertices, codes):
            offset = 1.0 / self.num_rows
            shape_vertices = self.shape_vertices * offset * self.size
            shape_codes = self.shape_codes
            if not self.filled:
                shape_vertices = np.concatenate(  # Forward, then backward.
                    [shape_vertices, shape_vertices[::-1] * 0.9])
                shape_codes = np.concatenate([shape_codes, shape_codes])
            vertices_parts = []
            codes_parts = []
            for row in range(self.num_rows + 1):
    
                cols = np.linspace(0, 1, self.num_rows + 1)
    
                row_pos = row * offset
                for col_pos in cols:
                    vertices_parts.append(shape_vertices + [col_pos, row_pos])
                    codes_parts.append(shape_codes)
            np.concatenate(vertices_parts, out=vertices)
            np.concatenate(codes_parts, out=codes)