hatch.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. """Contains classes for generating hatch patterns."""
  2. import numpy as np
  3. from matplotlib import _api
  4. from matplotlib.path import Path
  5. class HatchPatternBase:
  6. """The base class for a hatch pattern."""
  7. pass
  8. class HorizontalHatch(HatchPatternBase):
  9. def __init__(self, hatch, density):
  10. self.num_lines = int((hatch.count('-') + hatch.count('+')) * density)
  11. self.num_vertices = self.num_lines * 2
  12. def set_vertices_and_codes(self, vertices, codes):
  13. steps, stepsize = np.linspace(0.0, 1.0, self.num_lines, False,
  14. retstep=True)
  15. steps += stepsize / 2.
  16. vertices[0::2, 0] = 0.0
  17. vertices[0::2, 1] = steps
  18. vertices[1::2, 0] = 1.0
  19. vertices[1::2, 1] = steps
  20. codes[0::2] = Path.MOVETO
  21. codes[1::2] = Path.LINETO
  22. class VerticalHatch(HatchPatternBase):
  23. def __init__(self, hatch, density):
  24. self.num_lines = int((hatch.count('|') + hatch.count('+')) * density)
  25. self.num_vertices = self.num_lines * 2
  26. def set_vertices_and_codes(self, vertices, codes):
  27. steps, stepsize = np.linspace(0.0, 1.0, self.num_lines, False,
  28. retstep=True)
  29. steps += stepsize / 2.
  30. vertices[0::2, 0] = steps
  31. vertices[0::2, 1] = 0.0
  32. vertices[1::2, 0] = steps
  33. vertices[1::2, 1] = 1.0
  34. codes[0::2] = Path.MOVETO
  35. codes[1::2] = Path.LINETO
  36. class NorthEastHatch(HatchPatternBase):
  37. def __init__(self, hatch, density):
  38. self.num_lines = int(
  39. (hatch.count('/') + hatch.count('x') + hatch.count('X')) * density)
  40. if self.num_lines:
  41. self.num_vertices = (self.num_lines + 1) * 2
  42. else:
  43. self.num_vertices = 0
  44. def set_vertices_and_codes(self, vertices, codes):
  45. steps = np.linspace(-0.5, 0.5, self.num_lines + 1)
  46. vertices[0::2, 0] = 0.0 + steps
  47. vertices[0::2, 1] = 0.0 - steps
  48. vertices[1::2, 0] = 1.0 + steps
  49. vertices[1::2, 1] = 1.0 - steps
  50. codes[0::2] = Path.MOVETO
  51. codes[1::2] = Path.LINETO
  52. class SouthEastHatch(HatchPatternBase):
  53. def __init__(self, hatch, density):
  54. self.num_lines = int(
  55. (hatch.count('\\') + hatch.count('x') + hatch.count('X'))
  56. * density)
  57. if self.num_lines:
  58. self.num_vertices = (self.num_lines + 1) * 2
  59. else:
  60. self.num_vertices = 0
  61. def set_vertices_and_codes(self, vertices, codes):
  62. steps = np.linspace(-0.5, 0.5, self.num_lines + 1)
  63. vertices[0::2, 0] = 0.0 + steps
  64. vertices[0::2, 1] = 1.0 + steps
  65. vertices[1::2, 0] = 1.0 + steps
  66. vertices[1::2, 1] = 0.0 + steps
  67. codes[0::2] = Path.MOVETO
  68. codes[1::2] = Path.LINETO
  69. class Shapes(HatchPatternBase):
  70. filled = False
  71. def __init__(self, hatch, density):
  72. if self.num_rows == 0:
  73. self.num_shapes = 0
  74. self.num_vertices = 0
  75. else:
  76. self.num_shapes = ((self.num_rows // 2 + 1) * (self.num_rows + 1) +
  77. (self.num_rows // 2) * self.num_rows)
  78. self.num_vertices = (self.num_shapes *
  79. len(self.shape_vertices) *
  80. (1 if self.filled else 2))
  81. def set_vertices_and_codes(self, vertices, codes):
  82. offset = 1.0 / self.num_rows
  83. shape_vertices = self.shape_vertices * offset * self.size
  84. shape_codes = self.shape_codes
  85. if not self.filled:
  86. shape_vertices = np.concatenate( # Forward, then backward.
  87. [shape_vertices, shape_vertices[::-1] * 0.9])
  88. shape_codes = np.concatenate([shape_codes, shape_codes])
  89. vertices_parts = []
  90. codes_parts = []
  91. for row in range(self.num_rows + 1):
  92. if row % 2 == 0:
  93. cols = np.linspace(0, 1, self.num_rows + 1)
  94. else:
  95. cols = np.linspace(offset / 2, 1 - offset / 2, self.num_rows)
  96. row_pos = row * offset
  97. for col_pos in cols:
  98. vertices_parts.append(shape_vertices + [col_pos, row_pos])
  99. codes_parts.append(shape_codes)
  100. np.concatenate(vertices_parts, out=vertices)
  101. np.concatenate(codes_parts, out=codes)
  102. class Circles(Shapes):
  103. def __init__(self, hatch, density):
  104. path = Path.unit_circle()
  105. self.shape_vertices = path.vertices
  106. self.shape_codes = path.codes
  107. super().__init__(hatch, density)
  108. class SmallCircles(Circles):
  109. size = 0.2
  110. def __init__(self, hatch, density):
  111. self.num_rows = (hatch.count('o')) * density
  112. super().__init__(hatch, density)
  113. class LargeCircles(Circles):
  114. size = 0.35
  115. def __init__(self, hatch, density):
  116. self.num_rows = (hatch.count('O')) * density
  117. super().__init__(hatch, density)
  118. class SmallFilledCircles(Circles):
  119. size = 0.1
  120. filled = True
  121. def __init__(self, hatch, density):
  122. self.num_rows = (hatch.count('.')) * density
  123. super().__init__(hatch, density)
  124. class Stars(Shapes):
  125. size = 1.0 / 3.0
  126. filled = True
  127. def __init__(self, hatch, density):
  128. self.num_rows = (hatch.count('*')) * density
  129. path = Path.unit_regular_star(5)
  130. self.shape_vertices = path.vertices
  131. self.shape_codes = np.full(len(self.shape_vertices), Path.LINETO,
  132. dtype=Path.code_type)
  133. self.shape_codes[0] = Path.MOVETO
  134. super().__init__(hatch, density)
  135. _hatch_types = [
  136. HorizontalHatch,
  137. VerticalHatch,
  138. NorthEastHatch,
  139. SouthEastHatch,
  140. SmallCircles,
  141. LargeCircles,
  142. SmallFilledCircles,
  143. Stars
  144. ]
  145. def _validate_hatch_pattern(hatch):
  146. valid_hatch_patterns = set(r'-+|/\xXoO.*')
  147. if hatch is not None:
  148. invalids = set(hatch).difference(valid_hatch_patterns)
  149. if invalids:
  150. valid = ''.join(sorted(valid_hatch_patterns))
  151. invalids = ''.join(sorted(invalids))
  152. _api.warn_deprecated(
  153. '3.4',
  154. removal='3.11', # one release after custom hatches (#20690)
  155. message=f'hatch must consist of a string of "{valid}" or '
  156. 'None, but found the following invalid values '
  157. f'"{invalids}". Passing invalid values is deprecated '
  158. 'since %(since)s and will become an error in %(removal)s.'
  159. )
  160. def get_path(hatchpattern, density=6):
  161. """
  162. Given a hatch specifier, *hatchpattern*, generates Path to render
  163. the hatch in a unit square. *density* is the number of lines per
  164. unit square.
  165. """
  166. density = int(density)
  167. patterns = [hatch_type(hatchpattern, density)
  168. for hatch_type in _hatch_types]
  169. num_vertices = sum([pattern.num_vertices for pattern in patterns])
  170. if num_vertices == 0:
  171. return Path(np.empty((0, 2)))
  172. vertices = np.empty((num_vertices, 2))
  173. codes = np.empty(num_vertices, Path.code_type)
  174. cursor = 0
  175. for pattern in patterns:
  176. if pattern.num_vertices != 0:
  177. vertices_chunk = vertices[cursor:cursor + pattern.num_vertices]
  178. codes_chunk = codes[cursor:cursor + pattern.num_vertices]
  179. pattern.set_vertices_and_codes(vertices_chunk, codes_chunk)
  180. cursor += pattern.num_vertices
  181. return Path(vertices, codes)