Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e83d1f2

Browse files
committed
barebones impl for custom hatch
1 parent 35d841b commit e83d1f2

File tree

8 files changed

+284
-7
lines changed

8 files changed

+284
-7
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,7 @@ def __init__(self):
695695
self._hatch = None
696696
self._hatch_color = None
697697
self._hatch_linewidth = rcParams['hatch.linewidth']
698+
self._hatchstyle = []
698699
self._url = None
699700
self._gid = None
700701
self._snap = None
@@ -712,6 +713,7 @@ def copy_properties(self, gc):
712713
self._joinstyle = gc._joinstyle
713714
self._linestyle = gc._linestyle
714715
self._linewidth = gc._linewidth
716+
self._hatchstyle = gc._hatchstyle
715717
self._rgb = gc._rgb
716718
self._hatch = gc._hatch
717719
self._hatch_color = gc._hatch_color
@@ -937,6 +939,9 @@ def set_snap(self, snap):
937939
"""
938940
self._snap = snap
939941

942+
def set_hatch_buffer_scale(self, scale):
943+
self._hatch_buffer_scale = scale
944+
940945
def set_hatch(self, hatch):
941946
"""Set the hatch style (for fills)."""
942947
self._hatch = hatch
@@ -946,7 +951,9 @@ def get_hatch(self):
946951
return self._hatch
947952

948953
def get_hatch_path(self, density=6.0):
949-
"""Return a `.Path` for the current hatch."""
954+
"""Return a `.Path` for the current hatch or hatchstyle"""
955+
if len(self.get_hatchstyle()):
956+
return Path.hatchstyle(self.get_hatchstyle(), self._hatch_buffer_scale)
950957
hatch = self.get_hatch()
951958
if hatch is None:
952959
return None
@@ -968,6 +975,14 @@ def set_hatch_linewidth(self, hatch_linewidth):
968975
"""Set the hatch linewidth."""
969976
self._hatch_linewidth = hatch_linewidth
970977

978+
def get_hatchstyle(self):
979+
"""Get the hatch style."""
980+
return self._hatchstyle
981+
982+
def set_hatchstyle(self, hatchstyle):
983+
"""Set the hatch style."""
984+
self._hatchstyle = hatchstyle
985+
971986
def get_sketch_params(self):
972987
"""
973988
Return the sketch parameters for the artist.

lib/matplotlib/backends/backend_agg.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ def __init__(self, width, height, dpi):
6666
self.dpi = dpi
6767
self.width = width
6868
self.height = height
69-
self._renderer = _RendererAgg(int(width), int(height), dpi)
69+
hatchstyles_enabled = True
70+
self.hatch_buffer_scale = (
71+
(max(width, height) / dpi) if hatchstyles_enabled else 1.0
72+
)
73+
self._renderer = _RendererAgg(int(width), int(height), dpi,
74+
self.hatch_buffer_scale)
7075
self._filter_renderers = []
7176

7277
self._update_methods()
@@ -94,6 +99,7 @@ def draw_path(self, gc, path, transform, rgbFace=None):
9499
# docstring inherited
95100
nmax = mpl.rcParams['agg.path.chunksize'] # here at least for testing
96101
npts = path.vertices.shape[0]
102+
gc.set_hatch_buffer_scale(self.hatch_buffer_scale)
97103

98104
if (npts > nmax > 100 and path.should_simplify and
99105
rgbFace is None and gc.get_hatch() is None):

lib/matplotlib/hatch.py

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,233 @@ def get_path(hatchpattern, density=6):
223223
cursor += pattern.num_vertices
224224

225225
return Path(vertices, codes)
226+
227+
228+
attrs = {
229+
"scale": 6,
230+
"weight": 1.0,
231+
"angle": 0.0,
232+
"random_rotation": False,
233+
"random_placement": False,
234+
"x_stagger": 0.5,
235+
"y_stagger": 0.0,
236+
"filled": True,
237+
}
238+
239+
240+
class HatchStyle:
241+
def __init__(self, hatchpattern, **kwargs):
242+
self.hatchpattern = hatchpattern
243+
self.kwargs = {
244+
attr: kwargs.get(attr, default) for attr, default in attrs.items()
245+
}
246+
247+
def rotate_vertices(self, vertices, angle=None, scale_correction=True):
248+
vertices = vertices.copy()
249+
250+
if angle is None:
251+
angle = self.kwargs["angle"]
252+
angle_rad = np.deg2rad(angle)
253+
254+
center = np.mean(vertices, axis=0)
255+
vertices -= center
256+
257+
if scale_correction:
258+
scaling = abs(np.sin(angle_rad)) + abs(np.cos(angle_rad))
259+
vertices *= scaling
260+
261+
rotation_matrix = np.array(
262+
[
263+
[np.cos(angle_rad), -np.sin(angle_rad)],
264+
[np.sin(angle_rad), np.cos(angle_rad)],
265+
]
266+
)
267+
vertices = np.dot(vertices, rotation_matrix)
268+
vertices += center
269+
return vertices
270+
271+
def get_vertices_and_codes(self, hatch_buffer_scale=1.0):
272+
self.hatch_buffer_scale = hatch_buffer_scale
273+
vertices, codes = np.empty((0, 2)), np.empty(0, Path.code_type)
274+
275+
if self.hatchpattern in hatchpatterns:
276+
# This is for line hatches
277+
for func in np.atleast_1d(hatchpatterns[self.hatchpattern]):
278+
vertices_part, codes_part = func(self)
279+
vertices_part = self.rotate_vertices(vertices_part)
280+
281+
vertices = np.concatenate((vertices, vertices_part))
282+
codes = np.concatenate((codes, codes_part))
283+
else:
284+
# This is for marker hatches
285+
if self.hatchpattern not in MarkerHatchStyle.marker_paths:
286+
raise ValueError(f"Unknown hatch pattern: {self.hatchpattern}")
287+
func = MarkerHatchStyle.marker_pattern
288+
vertices_part, codes_part = func(self)
289+
290+
vertices = np.concatenate((vertices, vertices_part))
291+
codes = np.concatenate((codes, codes_part))
292+
293+
return vertices, codes
294+
295+
296+
class MarkerHatchStyle(HatchStyle):
297+
marker_paths = {
298+
"o": Path.unit_circle,
299+
"O": Path.unit_circle,
300+
"*": (Path.unit_regular_star, 5), # TODO: is there a better way to do this?
301+
}
302+
303+
# TODO: saner defaults or no?
304+
marker_sizes = {
305+
"o": 0.2,
306+
"O": 0.35,
307+
"*": 1.0 / 3.0,
308+
}
309+
310+
def _get_marker_path(marker):
311+
func = np.atleast_1d(MarkerHatchStyle.marker_paths[marker])
312+
path = func[0](*func[1:])
313+
size = MarkerHatchStyle.marker_sizes.get(marker, attrs["weight"])
314+
315+
return Path(
316+
vertices=path.vertices * size,
317+
codes=path.codes,
318+
)
319+
320+
def marker_pattern(hatchstyle):
321+
size = hatchstyle.kwargs["weight"]
322+
num_rows = round(hatchstyle.kwargs["scale"] * hatchstyle.hatch_buffer_scale)
323+
path = MarkerHatchStyle._get_marker_path(hatchstyle.hatchpattern)
324+
marker_vertices = hatchstyle.rotate_vertices(
325+
path.vertices, scale_correction=False
326+
)
327+
marker_codes = path.codes
328+
329+
offset = 1.0 / num_rows
330+
marker_vertices = marker_vertices * offset * size
331+
x_stagger = hatchstyle.kwargs["x_stagger"] * offset
332+
y_stagger = hatchstyle.kwargs["y_stagger"] * offset
333+
334+
if not hatchstyle.kwargs["filled"]:
335+
marker_vertices = np.concatenate(
336+
[marker_vertices, marker_vertices[::-1] * 0.9]
337+
)
338+
marker_codes = np.concatenate([marker_codes, marker_codes])
339+
340+
vertices = np.empty((0, 2))
341+
codes = np.empty(0, Path.code_type)
342+
for row in range(num_rows + 1):
343+
row_pos = row * offset
344+
if row % 2 == 0:
345+
cols = np.linspace(0, 1, num_rows + 1)
346+
else:
347+
cols = np.linspace(x_stagger, 1 + x_stagger, num_rows + 1)
348+
349+
for i, col_pos in enumerate(cols):
350+
vertices_part = marker_vertices + [col_pos, row_pos]
351+
if i % 2 == 1:
352+
vertices_part += [0, y_stagger]
353+
354+
if hatchstyle.kwargs["random_rotation"]:
355+
vertices_part = hatchstyle.rotate_vertices(
356+
vertices_part, np.random.uniform(0, 360), scale_correction=False
357+
)
358+
359+
if hatchstyle.kwargs["random_placement"]:
360+
vertices_part += np.random.uniform(-offset / 4, offset / 4, 2)
361+
362+
vertices = np.concatenate((vertices, vertices_part))
363+
codes = np.concatenate((codes, marker_codes))
364+
365+
return vertices, codes
366+
367+
368+
class LineHatchStyle(HatchStyle):
369+
def horizontal(hatchstyle):
370+
num_lines = round(hatchstyle.kwargs["scale"] * hatchstyle.hatch_buffer_scale)
371+
if num_lines:
372+
num_vertices = num_lines * 2
373+
else:
374+
num_vertices = 0
375+
376+
vertices = np.empty((num_vertices, 2))
377+
codes = np.empty(num_vertices, Path.code_type)
378+
steps, stepsize = np.linspace(0.0, 1.0, num_lines, False, retstep=True)
379+
steps += stepsize / 2.0
380+
vertices[0::2, 0] = 0.0
381+
vertices[0::2, 1] = steps
382+
vertices[1::2, 0] = 1.0
383+
vertices[1::2, 1] = steps
384+
codes[0::2] = Path.MOVETO
385+
codes[1::2] = Path.LINETO
386+
387+
return vertices, codes
388+
389+
def vertical(hatchstyle):
390+
num_lines = round(hatchstyle.kwargs["scale"] * hatchstyle.hatch_buffer_scale)
391+
if num_lines:
392+
num_vertices = num_lines * 2
393+
else:
394+
num_vertices = 0
395+
396+
vertices = np.empty((num_vertices, 2))
397+
codes = np.empty(num_vertices, Path.code_type)
398+
steps, stepsize = np.linspace(0.0, 1.0, num_lines, False, retstep=True)
399+
steps += stepsize / 2.0
400+
vertices[0::2, 0] = steps
401+
vertices[0::2, 1] = 0.0
402+
vertices[1::2, 0] = steps
403+
vertices[1::2, 1] = 1.0
404+
codes[0::2] = Path.MOVETO
405+
codes[1::2] = Path.LINETO
406+
407+
return vertices, codes
408+
409+
def north_east(hatchstyle):
410+
num_lines = round(hatchstyle.kwargs["scale"] * hatchstyle.hatch_buffer_scale)
411+
if num_lines:
412+
num_vertices = (num_lines + 1) * 2
413+
else:
414+
num_vertices = 0
415+
416+
vertices = np.empty((num_vertices, 2))
417+
codes = np.empty(num_vertices, Path.code_type)
418+
steps = np.linspace(-0.5, 0.5, num_lines + 1)
419+
vertices[0::2, 0] = 0.0 + steps
420+
vertices[0::2, 1] = 0.0 - steps
421+
vertices[1::2, 0] = 1.0 + steps
422+
vertices[1::2, 1] = 1.0 - steps
423+
codes[0::2] = Path.MOVETO
424+
codes[1::2] = Path.LINETO
425+
426+
return vertices, codes
427+
428+
def south_east(hatchstyle):
429+
num_lines = round(hatchstyle.kwargs["scale"] * hatchstyle.hatch_buffer_scale)
430+
if num_lines:
431+
num_vertices = (num_lines + 1) * 2
432+
else:
433+
num_vertices = 0
434+
435+
vertices = np.empty((num_vertices, 2))
436+
codes = np.empty(num_vertices, Path.code_type)
437+
steps = np.linspace(-0.5, 0.5, num_lines + 1)
438+
vertices[0::2, 0] = 0.0 + steps
439+
vertices[0::2, 1] = 1.0 + steps
440+
vertices[1::2, 0] = 1.0 + steps
441+
vertices[1::2, 1] = 0.0 + steps
442+
codes[0::2] = Path.MOVETO
443+
codes[1::2] = Path.LINETO
444+
445+
return vertices, codes
446+
447+
448+
hatchpatterns = {
449+
"-": LineHatchStyle.horizontal,
450+
"|": LineHatchStyle.vertical,
451+
"/": LineHatchStyle.north_east,
452+
"\\": LineHatchStyle.south_east,
453+
"+": (LineHatchStyle.horizontal, LineHatchStyle.vertical),
454+
"x": (LineHatchStyle.north_east, LineHatchStyle.south_east),
455+
}

lib/matplotlib/patches.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(self, *,
5757
capstyle=None,
5858
joinstyle=None,
5959
hatchcolor=None,
60+
hatchstyle=None,
6061
**kwargs):
6162
"""
6263
The following kwarg properties are supported
@@ -95,6 +96,7 @@ def __init__(self, *,
9596
self.set_hatch(hatch)
9697
self.set_capstyle(capstyle)
9798
self.set_joinstyle(joinstyle)
99+
self.set_hatchstyle(hatchstyle)
98100

99101
if len(kwargs):
100102
self._internal_update(kwargs)
@@ -603,6 +605,17 @@ def get_hatch_linewidth(self):
603605
"""Return the hatch linewidth."""
604606
return self._hatch_linewidth
605607

608+
def set_hatchstyle(self, hatchstyle):
609+
"""Set the hatchstyle(s) for the patch."""
610+
if hatchstyle is None:
611+
hatchstyle = []
612+
self._hatchstyle = np.atleast_1d(hatchstyle)
613+
self.stale = True
614+
615+
def get_hatchstyle(self):
616+
"""Return the hatchstyle(s)"""
617+
return self._hatchstyle
618+
606619
def _draw_paths_with_artist_properties(
607620
self, renderer, draw_path_args_list):
608621
"""
@@ -639,6 +652,11 @@ def _draw_paths_with_artist_properties(
639652
gc.set_hatch_color(self.get_hatchcolor())
640653
gc.set_hatch_linewidth(self._hatch_linewidth)
641654

655+
if self._hatchstyle.size > 0:
656+
gc.set_hatchstyle(self._hatchstyle)
657+
gc.set_hatch_color(self._hatch_color)
658+
gc.set_hatch_linewidth(self._hatch_linewidth)
659+
642660
if self.get_sketch_params() is not None:
643661
gc.set_sketch_params(*self.get_sketch_params())
644662

lib/matplotlib/path.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,14 @@ def hatch(hatchpattern, density=6):
10361036
return (get_path(hatchpattern, density)
10371037
if hatchpattern is not None else None)
10381038

1039+
def hatchstyle(hatchstyles, hatch_buffer_scale=1.0):
1040+
vertices, codes = np.empty((0, 2)), np.empty(0, Path.code_type)
1041+
for hatchstyle in hatchstyles:
1042+
verts, cods = hatchstyle.get_vertices_and_codes(hatch_buffer_scale)
1043+
vertices = np.concatenate([vertices, verts])
1044+
codes = np.concatenate([codes, cods])
1045+
return Path(vertices, codes)
1046+
10391047
def clip_to_bbox(self, bbox, inside=True):
10401048
"""
10411049
Clip the path to the given bounding box.

src/_backend_agg.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#include <Python.h>
66
#include "_backend_agg.h"
77

8-
RendererAgg::RendererAgg(unsigned int width, unsigned int height, double dpi)
8+
RendererAgg::RendererAgg(unsigned int width, unsigned int height, double dpi, double hatch_buffer_scale)
99
: width(width),
1010
height(height),
1111
dpi(dpi),
@@ -48,7 +48,7 @@ RendererAgg::RendererAgg(unsigned int width, unsigned int height, double dpi)
4848
rendererBase.clear(_fill_color);
4949
rendererAA.attach(rendererBase);
5050
rendererBin.attach(rendererBase);
51-
hatch_size = int(dpi);
51+
hatch_size = int(dpi * hatch_buffer_scale);
5252
hatchBuffer = new agg::int8u[hatch_size * hatch_size * 4];
5353
hatchRenderingBuffer.attach(hatchBuffer, hatch_size, hatch_size, hatch_size * 4);
5454
}

src/_backend_agg.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ class RendererAgg
127127
/* TODO: Remove facepair_t */
128128
typedef std::pair<bool, agg::rgba> facepair_t;
129129

130-
RendererAgg(unsigned int width, unsigned int height, double dpi);
130+
RendererAgg(unsigned int width, unsigned int height, double dpi, double hatch_buffer_scale=1.0);
131131

132132
virtual ~RendererAgg();
133133

0 commit comments

Comments
 (0)