"""Contains classes for generating hatch patterns.""" import numpy as np from matplotlib import _api from matplotlib.path import Path class HatchPatternBase: """The base class for a hatch pattern.""" pass class HorizontalHatch(HatchPatternBase): def __init__(self, hatch, density): self.num_lines = int((hatch.count('-') + hatch.count('+')) * density) self.num_vertices = self.num_lines * 2 def set_vertices_and_codes(self, vertices, codes): steps, stepsize = np.linspace(0.0, 1.0, self.num_lines, False, retstep=True) steps += stepsize / 2. vertices[0::2, 0] = 0.0 vertices[0::2, 1] = steps vertices[1::2, 0] = 1.0 vertices[1::2, 1] = steps codes[0::2] = Path.MOVETO codes[1::2] = Path.LINETO class VerticalHatch(HatchPatternBase): def __init__(self, hatch, density): self.num_lines = int((hatch.count('|') + hatch.count('+')) * density) self.num_vertices = self.num_lines * 2 def set_vertices_and_codes(self, vertices, codes): steps, stepsize = np.linspace(0.0, 1.0, self.num_lines, False, retstep=True) steps += stepsize / 2. vertices[0::2, 0] = steps vertices[0::2, 1] = 0.0 vertices[1::2, 0] = steps vertices[1::2, 1] = 1.0 codes[0::2] = Path.MOVETO codes[1::2] = Path.LINETO class NorthEastHatch(HatchPatternBase): def __init__(self, hatch, density): self.num_lines = int( (hatch.count('/') + hatch.count('x') + hatch.count('X')) * density) if self.num_lines: self.num_vertices = (self.num_lines + 1) * 2 else: self.num_vertices = 0 def set_vertices_and_codes(self, vertices, codes): steps = np.linspace(-0.5, 0.5, self.num_lines + 1) vertices[0::2, 0] = 0.0 + steps vertices[0::2, 1] = 0.0 - steps vertices[1::2, 0] = 1.0 + steps vertices[1::2, 1] = 1.0 - steps codes[0::2] = Path.MOVETO codes[1::2] = Path.LINETO class SouthEastHatch(HatchPatternBase): def __init__(self, hatch, density): self.num_lines = int( (hatch.count('\\') + hatch.count('x') + hatch.count('X')) * density) if self.num_lines: self.num_vertices = (self.num_lines + 1) * 2 else: self.num_vertices = 0 def set_vertices_and_codes(self, vertices, codes): steps = np.linspace(-0.5, 0.5, self.num_lines + 1) vertices[0::2, 0] = 0.0 + steps vertices[0::2, 1] = 1.0 + steps vertices[1::2, 0] = 1.0 + steps vertices[1::2, 1] = 0.0 + steps codes[0::2] = Path.MOVETO codes[1::2] = Path.LINETO class Shapes(HatchPatternBase): filled = False def __init__(self, hatch, density): if self.num_rows == 0: self.num_shapes = 0 self.num_vertices = 0 else: self.num_shapes = ((self.num_rows // 2 + 1) * (self.num_rows + 1) + (self.num_rows // 2) * self.num_rows) 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 if not self.filled: inner_vertices = shape_vertices[::-1] * 0.9 shape_codes = self.shape_codes shape_size = len(shape_vertices) cursor = 0 for row in range(self.num_rows + 1): 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) row_pos = row * offset for col_pos in cols: vertices[cursor:cursor + shape_size] = (shape_vertices + (col_pos, row_pos)) codes[cursor:cursor + shape_size] = shape_codes cursor += shape_size if not self.filled: vertices[cursor:cursor + shape_size] = (inner_vertices + (col_pos, row_pos)) codes[cursor:cursor + shape_size] = shape_codes cursor += shape_size class Circles(Shapes): def __init__(self, hatch, density): path = Path.unit_circle() self.shape_vertices = path.vertices self.shape_codes = path.codes super().__init__(hatch, density) class SmallCircles(Circles): size = 0.2 def __init__(self, hatch, density): self.num_rows = (hatch.count('o')) * density super().__init__(hatch, density) class LargeCircles(Circles): size = 0.35 def __init__(self, hatch, density): self.num_rows = (hatch.count('O')) * density super().__init__(hatch, density) # TODO: __init__ and class attributes override all attributes set by # SmallCircles. Should this class derive from Circles instead? class SmallFilledCircles(SmallCircles): size = 0.1 filled = True def __init__(self, hatch, density): self.num_rows = (hatch.count('.')) * density # Not super().__init__! Circles.__init__(self, hatch, density) class Stars(Shapes): size = 1.0 / 3.0 filled = True def __init__(self, hatch, density): self.num_rows = (hatch.count('*')) * density path = Path.unit_regular_star(5) self.shape_vertices = path.vertices self.shape_codes = np.full(len(self.shape_vertices), Path.LINETO, dtype=Path.code_type) self.shape_codes[0] = Path.MOVETO super().__init__(hatch, density) _hatch_types = [ HorizontalHatch, VerticalHatch, NorthEastHatch, SouthEastHatch, SmallCircles, LargeCircles, SmallFilledCircles, Stars ] def _validate_hatch_pattern(hatch): valid_hatch_patterns = set(r'-+|/\xXoO.*') if hatch is not None: invalids = set(hatch).difference(valid_hatch_patterns) if invalids: valid = ''.join(sorted(valid_hatch_patterns)) invalids = ''.join(sorted(invalids)) _api.warn_deprecated( '3.4', message=f'hatch must consist of a string of "{valid}" or ' 'None, but found the following invalid values ' f'"{invalids}". Passing invalid values is deprecated ' 'since %(since)s and will become an error %(removal)s.' ) def get_path(hatchpattern, density=6): """ Given a hatch specifier, *hatchpattern*, generates Path to render the hatch in a unit square. *density* is the number of lines per unit square. """ density = int(density) patterns = [hatch_type(hatchpattern, density) for hatch_type in _hatch_types] num_vertices = sum([pattern.num_vertices for pattern in patterns]) if num_vertices == 0: return Path(np.empty((0, 2))) vertices = np.empty((num_vertices, 2)) codes = np.empty(num_vertices, Path.code_type) cursor = 0 for pattern in patterns: if pattern.num_vertices != 0: vertices_chunk = vertices[cursor:cursor + pattern.num_vertices] codes_chunk = codes[cursor:cursor + pattern.num_vertices] pattern.set_vertices_and_codes(vertices_chunk, codes_chunk) cursor += pattern.num_vertices return Path(vertices, codes)