Advertisement
Guest User

XKCDify with matplotlib

a guest
Mar 7th, 2014
261
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.28 KB | None | 0 0
  1. """
  2. XKCD plot generator
  3. -------------------
  4. Author: Jake Vanderplas
  5.  
  6. This is a script that will take any matplotlib line diagram, and convert it
  7. to an XKCD-style plot.  It will work for plots with line & text elements,
  8. including axes labels and titles (but not axes tick labels).
  9.  
  10. The idea for this comes from work by Damon McDougall
  11.  http://www.mail-archive.com/matplotlib-users@lists.sourceforge.net/msg25499.html
  12. """
  13. import numpy as np
  14. import pylab as pl
  15. from scipy import interpolate, signal
  16. import matplotlib.font_manager as fm
  17.  
  18.  
  19. # We need a special font for the code below.  It can be downloaded this way:
  20. import os
  21. import urllib2
  22. if not os.path.exists('Humor-Sans.ttf'):
  23.     fhandle = urllib2.urlopen('http://antiyawn.com/uploads/Humor-Sans.ttf')
  24.     open('Humor-Sans.ttf', 'wb').write(fhandle.read())
  25.  
  26.    
  27. def xkcd_line(x, y, xlim=None, ylim=None,
  28.               mag=1.0, f1=30, f2=0.05, f3=15):
  29.     """
  30.    Mimic a hand-drawn line from (x, y) data
  31.  
  32.    Parameters
  33.    ----------
  34.    x, y : array_like
  35.        arrays to be modified
  36.    xlim, ylim : data range
  37.        the assumed plot range for the modification.  If not specified,
  38.        they will be guessed from the  data
  39.    mag : float
  40.        magnitude of distortions
  41.    f1, f2, f3 : int, float, int
  42.        filtering parameters.  f1 gives the size of the window, f2 gives
  43.        the high-frequency cutoff, f3 gives the size of the filter
  44.    
  45.    Returns
  46.    -------
  47.    x, y : ndarrays
  48.        The modified lines
  49.    """
  50.     x = np.asarray(x)
  51.     y = np.asarray(y)
  52.    
  53.     # get limits for rescaling
  54.     if xlim is None:
  55.         xlim = (x.min(), x.max())
  56.     if ylim is None:
  57.         ylim = (y.min(), y.max())
  58.  
  59.     if xlim[1] == xlim[0]:
  60.         xlim = ylim
  61.        
  62.     if ylim[1] == ylim[0]:
  63.         ylim = xlim
  64.  
  65.     # scale the data
  66.     x_scaled = (x - xlim[0]) * 1. / (xlim[1] - xlim[0])
  67.     y_scaled = (y - ylim[0]) * 1. / (ylim[1] - ylim[0])
  68.  
  69.     # compute the total distance along the path
  70.     dx = x_scaled[1:] - x_scaled[:-1]
  71.     dy = y_scaled[1:] - y_scaled[:-1]
  72.     dist_tot = np.sum(np.sqrt(dx * dx + dy * dy))
  73.  
  74.     # number of interpolated points is proportional to the distance
  75.     Nu = int(200 * dist_tot)
  76.     u = np.arange(-1, Nu + 1) * 1. / (Nu - 1)
  77.  
  78.     # interpolate curve at sampled points
  79.     k = min(3, len(x) - 1)
  80.     res = interpolate.splprep([x_scaled, y_scaled], s=0, k=k)
  81.     x_int, y_int = interpolate.splev(u, res[0])
  82.  
  83.     # we'll perturb perpendicular to the drawn line
  84.     dx = x_int[2:] - x_int[:-2]
  85.     dy = y_int[2:] - y_int[:-2]
  86.     dist = np.sqrt(dx * dx + dy * dy)
  87.  
  88.     # create a filtered perturbation
  89.     coeffs = mag * np.random.normal(0, 0.01, len(x_int) - 2)
  90.     b = signal.firwin(f1, f2 * dist_tot, window=('kaiser', f3))
  91.     response = signal.lfilter(b, 1, coeffs)
  92.  
  93.     x_int[1:-1] += response * dy / dist
  94.     y_int[1:-1] += response * dx / dist
  95.  
  96.     # un-scale data
  97.     x_int = x_int[1:-1] * (xlim[1] - xlim[0]) + xlim[0]
  98.     y_int = y_int[1:-1] * (ylim[1] - ylim[0]) + ylim[0]
  99.    
  100.     return x_int, y_int
  101.  
  102.  
  103. def XKCDify(ax, mag=1.0,
  104.             f1=50, f2=0.01, f3=15,
  105.             bgcolor='w',
  106.             xaxis_loc=None,
  107.             yaxis_loc=None,
  108.             xaxis_arrow='+',
  109.             yaxis_arrow='+',
  110.             ax_extend=0.1,
  111.             expand_axes=False):
  112.     """Make axis look hand-drawn
  113.  
  114.    This adjusts all lines, text, legends, and axes in the figure to look
  115.    like xkcd plots.  Other plot elements are not modified.
  116.    
  117.    Parameters
  118.    ----------
  119.    ax : Axes instance
  120.        the axes to be modified.
  121.    mag : float
  122.        the magnitude of the distortion
  123.    f1, f2, f3 : int, float, int
  124.        filtering parameters.  f1 gives the size of the window, f2 gives
  125.        the high-frequency cutoff, f3 gives the size of the filter
  126.    xaxis_loc, yaxis_log : float
  127.        The locations to draw the x and y axes.  If not specified, they
  128.        will be drawn from the bottom left of the plot
  129.    xaxis_arrow, yaxis_arrow : str
  130.        where to draw arrows on the x/y axes.  Options are '+', '-', '+-', or ''
  131.    ax_extend : float
  132.        How far (fractionally) to extend the drawn axes beyond the original
  133.        axes limits
  134.    expand_axes : bool
  135.        if True, then expand axes to fill the figure (useful if there is only
  136.        a single axes in the figure)
  137.    """
  138.     # Get axes aspect
  139.     ext = ax.get_window_extent().extents
  140.     aspect = (ext[3] - ext[1]) / (ext[2] - ext[0])
  141.  
  142.     xlim = ax.get_xlim()
  143.     ylim = ax.get_ylim()
  144.  
  145.     xspan = xlim[1] - xlim[0]
  146.     yspan = ylim[1] - xlim[0]
  147.  
  148.     xax_lim = (xlim[0] - ax_extend * xspan,
  149.                xlim[1] + ax_extend * xspan)
  150.     yax_lim = (ylim[0] - ax_extend * yspan,
  151.                ylim[1] + ax_extend * yspan)
  152.  
  153.     if xaxis_loc is None:
  154.         xaxis_loc = ylim[0]
  155.  
  156.     if yaxis_loc is None:
  157.         yaxis_loc = xlim[0]
  158.  
  159.     # Draw axes
  160.     xaxis = pl.Line2D([xax_lim[0], xax_lim[1]], [xaxis_loc, xaxis_loc],
  161.                       linestyle='-', color='k')
  162.     yaxis = pl.Line2D([yaxis_loc, yaxis_loc], [yax_lim[0], yax_lim[1]],
  163.                       linestyle='-', color='k')
  164.  
  165.     # Label axes3, 0.5, 'hello', fontsize=14)
  166.     ax.text(xax_lim[1], xaxis_loc - 0.02 * yspan, ax.get_xlabel(),
  167.             fontsize=14, ha='right', va='top', rotation=12)
  168.     ax.text(yaxis_loc - 0.02 * xspan, yax_lim[1], ax.get_ylabel(),
  169.             fontsize=14, ha='right', va='top', rotation=78)
  170.     ax.set_xlabel('')
  171.     ax.set_ylabel('')
  172.  
  173.     # Add title
  174.     ax.text(0.5 * (xax_lim[1] + xax_lim[0]), yax_lim[1],
  175.             ax.get_title(),
  176.             ha='center', va='bottom', fontsize=16)
  177.     ax.set_title('')
  178.  
  179.     Nlines = len(ax.lines)
  180.     lines = [xaxis, yaxis] + [ax.lines.pop(0) for i in range(Nlines)]
  181.  
  182.     for line in lines:
  183.         x, y = line.get_data()
  184.  
  185.         x_int, y_int = xkcd_line(x, y, xlim, ylim,
  186.                                  mag, f1, f2, f3)
  187.  
  188.         # create foreground and background line
  189.         lw = line.get_linewidth()
  190.         line.set_linewidth(2 * lw)
  191.         line.set_data(x_int, y_int)
  192.  
  193.         # don't add background line for axes
  194.         if (line is not xaxis) and (line is not yaxis):
  195.             line_bg = pl.Line2D(x_int, y_int, color=bgcolor,
  196.                                 linewidth=8 * lw)
  197.  
  198.             ax.add_line(line_bg)
  199.         ax.add_line(line)
  200.  
  201.     # Draw arrow-heads at the end of axes lines
  202.     arr1 = 0.03 * np.array([-1, 0, -1])
  203.     arr2 = 0.02 * np.array([-1, 0, 1])
  204.  
  205.     arr1[::2] += np.random.normal(0, 0.005, 2)
  206.     arr2[::2] += np.random.normal(0, 0.005, 2)
  207.  
  208.     x, y = xaxis.get_data()
  209.     if '+' in str(xaxis_arrow):
  210.         ax.plot(x[-1] + arr1 * xspan * aspect,
  211.                 y[-1] + arr2 * yspan,
  212.                 color='k', lw=2)
  213.     if '-' in str(xaxis_arrow):
  214.         ax.plot(x[0] - arr1 * xspan * aspect,
  215.                 y[0] - arr2 * yspan,
  216.                 color='k', lw=2)
  217.  
  218.     x, y = yaxis.get_data()
  219.     if '+' in str(yaxis_arrow):
  220.         ax.plot(x[-1] + arr2 * xspan * aspect,
  221.                 y[-1] + arr1 * yspan,
  222.                 color='k', lw=2)
  223.     if '-' in str(yaxis_arrow):
  224.         ax.plot(x[0] - arr2 * xspan * aspect,
  225.                 y[0] - arr1 * yspan,
  226.                 color='k', lw=2)
  227.  
  228.     # Change all the fonts to humor-sans.
  229.     prop = fm.FontProperties(fname='Humor-Sans.ttf', size=16)
  230.     for text in ax.texts:
  231.         text.set_fontproperties(prop)
  232.    
  233.     # modify legend
  234.     leg = ax.get_legend()
  235.     if leg is not None:
  236.         leg.set_frame_on(False)
  237.        
  238.         for child in leg.get_children():
  239.             if isinstance(child, pl.Line2D):
  240.                 x, y = child.get_data()
  241.                 child.set_data(xkcd_line(x, y, mag=10, f1=100, f2=0.001))
  242.                 child.set_linewidth(2 * child.get_linewidth())
  243.             if isinstance(child, pl.Text):
  244.                 child.set_fontproperties(prop)
  245.    
  246.     # Set the axis limits
  247.     ax.set_xlim(xax_lim[0] - 0.1 * xspan,
  248.                 xax_lim[1] + 0.1 * xspan)
  249.     ax.set_ylim(yax_lim[0] - 0.1 * yspan,
  250.                 yax_lim[1] + 0.1 * yspan)
  251.  
  252.     # adjust the axes
  253.     ax.set_xticks([])
  254.     ax.set_yticks([])      
  255.  
  256.     if expand_axes:
  257.         ax.figure.set_facecolor(bgcolor)
  258.         ax.set_axis_off()
  259.         ax.set_position([0, 0, 1, 1])
  260.    
  261.     return ax
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement