Table of Contents

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!

Table of Contents
Table of Contents