Happy Thanksgiving!
Hi all, for the upcoming US holiday, I wanted to share some some turkey with all of you! Actually though, which I managed to make a turkey in everyone's favorite drawing tool matplotlib.
While I would not recommend doing this, it was a fun way to learn more about some of the lower level interfaces that matplotlib offers. I hope you all enjoy the holiday if you are celebrating!
The Original
After scouring google images for hand turkeys, I finally found one that is worthy to recreate in matplotlib.
The Matplotlib Effort
I have to admit, drawing this by hand with some crayons and a pencil would have been much easier- but this was a fun adventure into some of the lower level APIs that matplotlib has to offer.
from matplotlib.pyplot import subplots, show, rc, imread
from matplotlib.patches import PathPatch, Circle, Rectangle, RegularPolygon, Path
from matplotlib import patheffects
from numpy import pi
from itertools import islice, tee, repeat, chain, repeat, zip_longest
import pathlib
nwise = lambda g, n=2: zip(*(islice(g, i, None) for i, g in enumerate(tee(g, n))))
nwise_longest = lambda g, n=2, fv=object(): zip_longest(*(islice(g, i, None) for i, g in enumerate(tee(g, n))), fillvalue=fv)
first = lambda g, n=1: zip(chain(repeat(True, n), repeat(False)), g)
last = lambda g, m=1, s=object(): ((y[-1] is s, x) for x, *y in nwise_longest(g, m+1, s))
polys = {
'ring': ('green', [(.3, .54), (.33, .8), (.37, .9), (.39, .8), (.39, .54), (.3, .54),]),
'middle': ('red', [(.4, .54), (.43, .8), (.47, .9), (.49, .8), (.49, .54), (.4, .54),]),
'index': ('orange', [(.5, .54), (.53, .8), (.57, .9), (.59, .8), (.59, .5), (.5, .54),]),
'palm': ('#a0522d', [(.3, .54), (.5, .7), (.6, .5), (.55, .3), (.7, .5), (.8, .55), (.8, .5),
(.6, .2), (.5, .15), (.45, .15), (.37, .15), (.35, .18), (.32, .2),
(.28, .4), (.28, .4), (.3, .54),],),
}
patches = {
'eye': Circle((.75, .49), radius=.01, facecolor='black'),
'beak': RegularPolygon((.79, .51), numVertices=3, radius=.03, orientation=pi/3, facecolor='orange', zorder=-1),
'snood': RegularPolygon((.74, .41), numVertices=3, radius=.03, orientation=0, facecolor='red', zorder=-1),
'hat': Rectangle((.67, .49), width=.1, height=.05, angle=25, facecolor='black'),
'brim': Rectangle((.64, .47), width=.16, height=.01, angle=25, facecolor='black'),
'buckle': Rectangle((.70, .515), width=.03, height=.03, angle=25, facecolor='goldenrod'),
}
lines = {
'left leg': ([.4, .4], [.05, .15]),
'left hallux': ([.38, .4], [.05, .07]),
'left pinky': ([.42, .4], [.05, .07]),
'right leg': ([.48, .48], [.05, .15]),
'right hallux': ([.46, .48], [.05, .07]),
'right pinky': ([.5, .48], [.05, .07]),
}
rc('axes.spines', top=False, left=False, bottom=False, right=False)
rc('xtick', bottom=False, labelbottom=False)
rc('ytick', left=False, labelleft=False)
fig, ax = subplots(dpi=140, figsize=(8, 4), gridspec_kw={'bottom': .1})
fig.set_facecolor('white')
for col, verts in polys.values():
codes = [Path.MOVETO if is_first else Path.CLOSEPOLY if is_last else Path.CURVE3 for is_first, (is_last, _) in first(last(verts))]
path = Path(verts, codes)
ax.add_patch(PathPatch(path, facecolor=col))
for p in patches.values():
ax.add_artist(p)
for xs, ys in lines.values():
ax.plot(xs, ys, zorder=-1, color='black', lw=3)
# Title inside of `Axes`
ax.text(
.5, .9, 'Happy Thanksgiving from', transform=ax.get_xaxis_transform(),
ha='center', size=36, color='#b8860b',
font=pathlib.Path('../_static/Interstate/Interstate Regular.otf'),
path_effects=[patheffects.withSimplePatchShadow()]
)
# Add Logo
logo_ax = fig.add_axes([0, 0, 1, ax.get_gridspec().get_subplot_params().bottom])
image_data = imread('../_static/images/DUTC_Logo_Horizontal_1001x61.png')
logo_ax.set_anchor('S')
logo_ax.imshow(image_data)
# Add personal watermark
ax.text(.64, .15, 'Cameron Riddell @RiddleMeCam', color='lightgray')
ax.set_aspect(1/2, anchor='C')
Error in callback <function _draw_all_if_interactive at 0x7ba6c70584a0> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/pyplot.py:265, in _draw_all_if_interactive()
263 def _draw_all_if_interactive() -> None:
264 if matplotlib.is_interactive():
--> 265 draw_all()
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
129 for manager in cls.get_all_fig_managers():
130 if force or manager.canvas.figure.stale:
--> 131 manager.canvas.draw_idle()
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/backend_bases.py:1919, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
1917 if not self._is_idle_drawing:
1918 with self._idle_draw_cntx():
-> 1919 self.draw(*args, **kwargs)
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/backends/backend_agg.py:387, in FigureCanvasAgg.draw(self)
384 # Acquire a lock on the shared font cache.
385 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
386 else nullcontext()):
--> 387 self.figure.draw(self.renderer)
388 # A GUI class may be need to update a window using this draw, so
389 # don't forget to call the superclass.
390 super().draw()
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/artist.py:95, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
93 @wraps(draw)
94 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 95 result = draw(artist, renderer, *args, **kwargs)
96 if renderer._rasterizing:
97 renderer.stop_rasterizing()
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
69 if artist.get_agg_filter() is not None:
70 renderer.start_filter()
---> 72 return draw(artist, renderer)
73 finally:
74 if artist.get_agg_filter() is not None:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/figure.py:3155, in Figure.draw(self, renderer)
3152 # ValueError can occur when resizing a window.
3154 self.patch.draw(renderer)
-> 3155 mimage._draw_list_compositing_images(
3156 renderer, self, artists, self.suppressComposite)
3158 renderer.close_group('figure')
3159 finally:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/image.py:132, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
130 if not_composite or not has_images:
131 for a in artists:
--> 132 a.draw(renderer)
133 else:
134 # Composite any adjacent images together
135 image_group = []
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
69 if artist.get_agg_filter() is not None:
70 renderer.start_filter()
---> 72 return draw(artist, renderer)
73 finally:
74 if artist.get_agg_filter() is not None:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/axes/_base.py:3109, in _AxesBase.draw(self, renderer)
3106 if artists_rasterized:
3107 _draw_rasterized(self.figure, artists_rasterized, renderer)
-> 3109 mimage._draw_list_compositing_images(
3110 renderer, self, artists, self.figure.suppressComposite)
3112 renderer.close_group('axes')
3113 self.stale = False
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/image.py:132, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
130 if not_composite or not has_images:
131 for a in artists:
--> 132 a.draw(renderer)
133 else:
134 # Composite any adjacent images together
135 image_group = []
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
69 if artist.get_agg_filter() is not None:
70 renderer.start_filter()
---> 72 return draw(artist, renderer)
73 finally:
74 if artist.get_agg_filter() is not None:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/text.py:748, in Text.draw(self, renderer)
745 renderer.open_group('text', self.get_gid())
747 with self._cm_set(text=self._get_wrapped_text()):
--> 748 bbox, info, descent = self._get_layout(renderer)
749 trans = self.get_transform()
751 # don't use self.get_position here, which refers to text
752 # position in Text:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
370 ys = []
372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
374 renderer, "lp", self._fontproperties,
375 ismath="TeX" if self.get_usetex() else False, dpi=self.figure.dpi)
376 min_dy = (lp_h - lp_d) * self._linespacing
378 for i, line in enumerate(lines):
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/backends/backend_agg.py:219, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
215 ox, oy, width, height, descent, font_image = \
216 self.mathtext_parser.parse(s, self.dpi, prop)
217 return width, height, descent
--> 219 font = self._prepare_font(prop)
220 font.set_text(s, 0.0, flags=get_hinting_flag())
221 w, h = font.get_width_height() # width and height of unrotated string
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/backends/backend_agg.py:253, in RendererAgg._prepare_font(self, font_prop)
249 def _prepare_font(self, font_prop):
250 """
251 Get the `.FT2Font` for *font_prop*, clear its buffer, and set its size.
252 """
--> 253 font = get_font(_fontManager._find_fonts_by_props(font_prop))
254 font.clear()
255 size = font_prop.get_size_in_points()
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/font_manager.py:1557, in get_font(font_filepaths, hinting_factor)
1554 if hinting_factor is None:
1555 hinting_factor = mpl.rcParams['text.hinting_factor']
-> 1557 return _get_font(
1558 # must be a tuple to be cached
1559 paths,
1560 hinting_factor,
1561 _kerning_factor=mpl.rcParams['text.kerning_factor'],
1562 # also key on the thread ID to prevent segfaults with multi-threading
1563 thread_id=threading.get_ident()
1564 )
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/font_manager.py:1499, in _get_font(font_filepaths, hinting_factor, _kerning_factor, thread_id)
1496 @lru_cache(64)
1497 def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id):
1498 first_fontpath, *rest = font_filepaths
-> 1499 return ft2font.FT2Font(
1500 first_fontpath, hinting_factor,
1501 _fallback_list=[
1502 ft2font.FT2Font(
1503 fpath, hinting_factor,
1504 _kerning_factor=_kerning_factor
1505 )
1506 for fpath in rest
1507 ],
1508 _kerning_factor=_kerning_factor
1509 )
FileNotFoundError: [Errno 2] No such file or directory: '/home/cameron/repos/dutc-io/company-site/site/_static/Interstate/Interstate Regular.otf'
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/IPython/core/formatters.py:343, in BaseFormatter.__call__(self, obj)
341 pass
342 else:
--> 343 return printer(obj)
344 # Finally look for special method names
345 method = get_real_method(obj, self.print_method)
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
167 from matplotlib.backend_bases import FigureCanvasBase
168 FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
171 data = bytes_io.getvalue()
172 if fmt == 'svg':
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/backend_bases.py:2189, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
2186 # we do this instead of `self.figure.draw_without_rendering`
2187 # so that we can inject the orientation
2188 with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2189 self.figure.draw(renderer)
2190 if bbox_inches:
2191 if bbox_inches == "tight":
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/artist.py:95, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
93 @wraps(draw)
94 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 95 result = draw(artist, renderer, *args, **kwargs)
96 if renderer._rasterizing:
97 renderer.stop_rasterizing()
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
69 if artist.get_agg_filter() is not None:
70 renderer.start_filter()
---> 72 return draw(artist, renderer)
73 finally:
74 if artist.get_agg_filter() is not None:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/figure.py:3155, in Figure.draw(self, renderer)
3152 # ValueError can occur when resizing a window.
3154 self.patch.draw(renderer)
-> 3155 mimage._draw_list_compositing_images(
3156 renderer, self, artists, self.suppressComposite)
3158 renderer.close_group('figure')
3159 finally:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/image.py:132, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
130 if not_composite or not has_images:
131 for a in artists:
--> 132 a.draw(renderer)
133 else:
134 # Composite any adjacent images together
135 image_group = []
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
69 if artist.get_agg_filter() is not None:
70 renderer.start_filter()
---> 72 return draw(artist, renderer)
73 finally:
74 if artist.get_agg_filter() is not None:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/axes/_base.py:3109, in _AxesBase.draw(self, renderer)
3106 if artists_rasterized:
3107 _draw_rasterized(self.figure, artists_rasterized, renderer)
-> 3109 mimage._draw_list_compositing_images(
3110 renderer, self, artists, self.figure.suppressComposite)
3112 renderer.close_group('axes')
3113 self.stale = False
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/image.py:132, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
130 if not_composite or not has_images:
131 for a in artists:
--> 132 a.draw(renderer)
133 else:
134 # Composite any adjacent images together
135 image_group = []
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
69 if artist.get_agg_filter() is not None:
70 renderer.start_filter()
---> 72 return draw(artist, renderer)
73 finally:
74 if artist.get_agg_filter() is not None:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/text.py:748, in Text.draw(self, renderer)
745 renderer.open_group('text', self.get_gid())
747 with self._cm_set(text=self._get_wrapped_text()):
--> 748 bbox, info, descent = self._get_layout(renderer)
749 trans = self.get_transform()
751 # don't use self.get_position here, which refers to text
752 # position in Text:
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
370 ys = []
372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
374 renderer, "lp", self._fontproperties,
375 ismath="TeX" if self.get_usetex() else False, dpi=self.figure.dpi)
376 min_dy = (lp_h - lp_d) * self._linespacing
378 for i, line in enumerate(lines):
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/backends/backend_agg.py:219, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
215 ox, oy, width, height, descent, font_image = \
216 self.mathtext_parser.parse(s, self.dpi, prop)
217 return width, height, descent
--> 219 font = self._prepare_font(prop)
220 font.set_text(s, 0.0, flags=get_hinting_flag())
221 w, h = font.get_width_height() # width and height of unrotated string
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/backends/backend_agg.py:253, in RendererAgg._prepare_font(self, font_prop)
249 def _prepare_font(self, font_prop):
250 """
251 Get the `.FT2Font` for *font_prop*, clear its buffer, and set its size.
252 """
--> 253 font = get_font(_fontManager._find_fonts_by_props(font_prop))
254 font.clear()
255 size = font_prop.get_size_in_points()
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/font_manager.py:1557, in get_font(font_filepaths, hinting_factor)
1554 if hinting_factor is None:
1555 hinting_factor = mpl.rcParams['text.hinting_factor']
-> 1557 return _get_font(
1558 # must be a tuple to be cached
1559 paths,
1560 hinting_factor,
1561 _kerning_factor=mpl.rcParams['text.kerning_factor'],
1562 # also key on the thread ID to prevent segfaults with multi-threading
1563 thread_id=threading.get_ident()
1564 )
File ~/.pyenv/versions/dutc-site-new/lib/python3.12/site-packages/matplotlib/font_manager.py:1499, in _get_font(font_filepaths, hinting_factor, _kerning_factor, thread_id)
1496 @lru_cache(64)
1497 def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id):
1498 first_fontpath, *rest = font_filepaths
-> 1499 return ft2font.FT2Font(
1500 first_fontpath, hinting_factor,
1501 _fallback_list=[
1502 ft2font.FT2Font(
1503 fpath, hinting_factor,
1504 _kerning_factor=_kerning_factor
1505 )
1506 for fpath in rest
1507 ],
1508 _kerning_factor=_kerning_factor
1509 )
FileNotFoundError: [Errno 2] No such file or directory: '/home/cameron/repos/dutc-io/company-site/site/_static/Interstate/Interstate Regular.otf'
<Figure size 1120x560 with 2 Axes>
Wrap Up
Not too bad! If you're curious about some resources I used to create this hand turkey, check them out on the documentation pages:
If you observe Thanksgiving, I hope you have a great holiday. Talk to you all again next week!