%% This file was auto-generated by IPython. %% Conversion from the original notebook file: %% tests/ipynbref/XKCD_plots.orig.ipynb %% \documentclass[11pt,english]{article} %% This is the automatic preamble used by IPython. Note that it does *not* %% include a documentclass declaration, that is added at runtime to the overall %% document. \usepackage{amsmath} \usepackage{amssymb} \usepackage{graphicx} \usepackage{ucs} \usepackage[utf8x]{inputenc} % needed for markdown enumerations to work \usepackage{enumerate} % Slightly bigger margins than the latex defaults \usepackage{geometry} \geometry{verbose,tmargin=3cm,bmargin=3cm,lmargin=2.5cm,rmargin=2.5cm} % Define a few colors for use in code, links and cell shading \usepackage{color} \definecolor{orange}{cmyk}{0,0.4,0.8,0.2} \definecolor{darkorange}{rgb}{.71,0.21,0.01} \definecolor{darkgreen}{rgb}{.12,.54,.11} \definecolor{myteal}{rgb}{.26, .44, .56} \definecolor{gray}{gray}{0.45} \definecolor{lightgray}{gray}{.95} \definecolor{mediumgray}{gray}{.8} \definecolor{inputbackground}{rgb}{.95, .95, .85} \definecolor{outputbackground}{rgb}{.95, .95, .95} \definecolor{traceback}{rgb}{1, .95, .95} % Framed environments for code cells (inputs, outputs, errors, ...). The % various uses of \unskip (or not) at the end were fine-tuned by hand, so don't % randomly change them unless you're sure of the effect it will have. \usepackage{framed} % remove extraneous vertical space in boxes \setlength\fboxsep{0pt} % codecell is the whole input+output set of blocks that a Code cell can % generate. % TODO: unfortunately, it seems that using a framed codecell environment breaks % the ability of the frames inside of it to be broken across pages. This % causes at least the problem of having lots of empty space at the bottom of % pages as new frames are moved to the next page, and if a single frame is too % long to fit on a page, will completely stop latex from compiling the % document. So unless we figure out a solution to this, we'll have to instead % leave the codecell env. as empty. I'm keeping the original codecell % definition here (a thin vertical bar) for reference, in case we find a % solution to the page break issue. %% \newenvironment{codecell}{% %% \def\FrameCommand{\color{mediumgray} \vrule width 1pt \hspace{5pt}}% %% \MakeFramed{\vspace{-0.5em}}} %% {\unskip\endMakeFramed} % For now, make this a no-op... \newenvironment{codecell}{} \newenvironment{codeinput}{% \def\FrameCommand{\colorbox{inputbackground}}% \MakeFramed{\advance\hsize-\width \FrameRestore}} {\unskip\endMakeFramed} \newenvironment{codeoutput}{% \def\FrameCommand{\colorbox{outputbackground}}% \vspace{-1.4em} \MakeFramed{\advance\hsize-\width \FrameRestore}} {\unskip\medskip\endMakeFramed} \newenvironment{traceback}{% \def\FrameCommand{\colorbox{traceback}}% \MakeFramed{\advance\hsize-\width \FrameRestore}} {\endMakeFramed} % Use and configure listings package for nicely formatted code \usepackage{listingsutf8} \lstset{ language=python, inputencoding=utf8x, extendedchars=\true, aboveskip=\smallskipamount, belowskip=\smallskipamount, xleftmargin=2mm, breaklines=true, basicstyle=\small \ttfamily, showstringspaces=false, keywordstyle=\color{blue}\bfseries, commentstyle=\color{myteal}, stringstyle=\color{darkgreen}, identifierstyle=\color{darkorange}, columns=fullflexible, % tighter character kerning, like verb } % The hyperref package gives us a pdf with properly built % internal navigation ('pdf bookmarks' for the table of contents, % internal cross-reference links, web links for URLs, etc.) \usepackage{hyperref} \hypersetup{ breaklinks=true, % so long urls are correctly broken across lines colorlinks=true, urlcolor=blue, linkcolor=darkorange, citecolor=darkgreen, } % hardcode size of all verbatim environments to be a bit smaller \makeatletter \g@addto@macro\@verbatim\small\topsep=0.5em\partopsep=0pt \makeatother % Prevent overflowing lines due to urls and other hard-to-break entities. \sloppy \begin{document} \section{XKCD plots in Matplotlib} This notebook originally appeared as a blog post at \href{http://jakevdp.github.com/blog/2012/10/07/xkcd-style-plots-in-matplotlib/}{Pythonic Perambulations} by Jake Vanderplas. One of the problems I've had with typical matplotlib figures is that everything in them is so precise, so perfect. For an example of what I mean, take a look at this figure: \begin{codecell} \begin{codeinput} \begin{lstlisting} from IPython.display import Image Image('http://jakevdp.github.com/figures/xkcd_version.png') \end{lstlisting} \end{codeinput} \begin{codeoutput} \begin{verbatim} \end{verbatim} \end{codeoutput} \end{codecell} Sometimes when showing schematic plots, this is the type of figure I want to display. But drawing it by hand is a pain: I'd rather just use matplotlib. The problem is, matplotlib is a bit too precise. Attempting to duplicate this figure in matplotlib leads to something like this: \begin{codecell} \begin{codeinput} \begin{lstlisting} Image('http://jakevdp.github.com/figures/mpl_version.png') \end{lstlisting} \end{codeinput} \begin{codeoutput} \begin{verbatim} \end{verbatim} \end{codeoutput} \end{codecell} It just doesn't have the same effect. Matplotlib is great for scientific plots, but sometimes you don't want to be so precise. This subject has recently come up on the matplotlib mailing list, and started some interesting discussions. As near as I can tell, this started with a thread on a \href{http://mathematica.stackexchange.com/questions/11350/xkcd-style-graphs}{mathematica list} which prompted a thread on the \href{http://matplotlib.1069221.n5.nabble.com/XKCD-style-graphs-td39226.html}{matplotlib list} wondering if the same could be done in matplotlib. Damon McDougall offered a quick \href{http://www.mail-archive.com/matplotlib-users@lists.sourceforge.net/msg25499.html}{solution} which was improved by Fernando Perez in \href{http://nbviewer.ipython.org/3835181/}{this notebook}, and within a few days there was a \href{https://github.com/matplotlib/matplotlib/pull/1329}{matplotlib pull request} offering a very general way to create sketch-style plots in matplotlib. Only a few days from a cool idea to a working implementation: this is one of the most incredible aspects of package development on github. The pull request looks really nice, but will likely not be included in a released version of matplotlib until at least version 1.3. In the mean-time, I wanted a way to play around with these types of plots in a way that is compatible with the current release of matplotlib. To do that, I created the following code: \subsection{The Code: XKCDify} XKCDify will take a matplotlib \texttt{Axes} instance, and modify the plot elements in-place to make them look hand-drawn. First off, we'll need to make sure we have the Humor Sans font. It can be downloaded using the command below. Next we'll create a function \texttt{xkcd\_line} to add jitter to lines. We want this to be very general, so we'll normalize the size of the lines, and use a low-pass filter to add correlated noise, perpendicular to the direction of the line. There are a few parameters for this filter that can be tweaked to customize the appearance of the jitter. Finally, we'll create a function which accepts a matplotlib axis, and calls \texttt{xkcd\_line} on all lines in the axis. Additionally, we'll switch the font of all text in the axes, and add some background lines for a nice effect where lines cross. We'll also draw axes, and move the axes labels and titles to the appropriate location. \begin{codecell} \begin{codeinput} \begin{lstlisting} """ XKCD plot generator ------------------- Author: Jake Vanderplas This is a script that will take any matplotlib line diagram, and convert it to an XKCD-style plot. It will work for plots with line & text elements, including axes labels and titles (but not axes tick labels). The idea for this comes from work by Damon McDougall http://www.mail-archive.com/matplotlib-users@lists.sourceforge.net/msg25499.html """ import numpy as np import pylab as pl from scipy import interpolate, signal import matplotlib.font_manager as fm # We need a special font for the code below. It can be downloaded this way: import os import urllib2 if not os.path.exists('Humor-Sans.ttf'): fhandle = urllib2.urlopen('http://antiyawn.com/uploads/Humor-Sans.ttf') open('Humor-Sans.ttf', 'wb').write(fhandle.read()) def xkcd_line(x, y, xlim=None, ylim=None, mag=1.0, f1=30, f2=0.05, f3=15): """ Mimic a hand-drawn line from (x, y) data Parameters ---------- x, y : array_like arrays to be modified xlim, ylim : data range the assumed plot range for the modification. If not specified, they will be guessed from the data mag : float magnitude of distortions f1, f2, f3 : int, float, int filtering parameters. f1 gives the size of the window, f2 gives the high-frequency cutoff, f3 gives the size of the filter Returns ------- x, y : ndarrays The modified lines """ x = np.asarray(x) y = np.asarray(y) # get limits for rescaling if xlim is None: xlim = (x.min(), x.max()) if ylim is None: ylim = (y.min(), y.max()) if xlim[1] == xlim[0]: xlim = ylim if ylim[1] == ylim[0]: ylim = xlim # scale the data x_scaled = (x - xlim[0]) * 1. / (xlim[1] - xlim[0]) y_scaled = (y - ylim[0]) * 1. / (ylim[1] - ylim[0]) # compute the total distance along the path dx = x_scaled[1:] - x_scaled[:-1] dy = y_scaled[1:] - y_scaled[:-1] dist_tot = np.sum(np.sqrt(dx * dx + dy * dy)) # number of interpolated points is proportional to the distance Nu = int(200 * dist_tot) u = np.arange(-1, Nu + 1) * 1. / (Nu - 1) # interpolate curve at sampled points k = min(3, len(x) - 1) res = interpolate.splprep([x_scaled, y_scaled], s=0, k=k) x_int, y_int = interpolate.splev(u, res[0]) # we'll perturb perpendicular to the drawn line dx = x_int[2:] - x_int[:-2] dy = y_int[2:] - y_int[:-2] dist = np.sqrt(dx * dx + dy * dy) # create a filtered perturbation coeffs = mag * np.random.normal(0, 0.01, len(x_int) - 2) b = signal.firwin(f1, f2 * dist_tot, window=('kaiser', f3)) response = signal.lfilter(b, 1, coeffs) x_int[1:-1] += response * dy / dist y_int[1:-1] += response * dx / dist # un-scale data x_int = x_int[1:-1] * (xlim[1] - xlim[0]) + xlim[0] y_int = y_int[1:-1] * (ylim[1] - ylim[0]) + ylim[0] return x_int, y_int def XKCDify(ax, mag=1.0, f1=50, f2=0.01, f3=15, bgcolor='w', xaxis_loc=None, yaxis_loc=None, xaxis_arrow='+', yaxis_arrow='+', ax_extend=0.1, expand_axes=False): """Make axis look hand-drawn This adjusts all lines, text, legends, and axes in the figure to look like xkcd plots. Other plot elements are not modified. Parameters ---------- ax : Axes instance the axes to be modified. mag : float the magnitude of the distortion f1, f2, f3 : int, float, int filtering parameters. f1 gives the size of the window, f2 gives the high-frequency cutoff, f3 gives the size of the filter xaxis_loc, yaxis_log : float The locations to draw the x and y axes. If not specified, they will be drawn from the bottom left of the plot xaxis_arrow, yaxis_arrow : str where to draw arrows on the x/y axes. Options are '+', '-', '+-', or '' ax_extend : float How far (fractionally) to extend the drawn axes beyond the original axes limits expand_axes : bool if True, then expand axes to fill the figure (useful if there is only a single axes in the figure) """ # Get axes aspect ext = ax.get_window_extent().extents aspect = (ext[3] - ext[1]) / (ext[2] - ext[0]) xlim = ax.get_xlim() ylim = ax.get_ylim() xspan = xlim[1] - xlim[0] yspan = ylim[1] - xlim[0] xax_lim = (xlim[0] - ax_extend * xspan, xlim[1] + ax_extend * xspan) yax_lim = (ylim[0] - ax_extend * yspan, ylim[1] + ax_extend * yspan) if xaxis_loc is None: xaxis_loc = ylim[0] if yaxis_loc is None: yaxis_loc = xlim[0] # Draw axes xaxis = pl.Line2D([xax_lim[0], xax_lim[1]], [xaxis_loc, xaxis_loc], linestyle='-', color='k') yaxis = pl.Line2D([yaxis_loc, yaxis_loc], [yax_lim[0], yax_lim[1]], linestyle='-', color='k') # Label axes3, 0.5, 'hello', fontsize=14) ax.text(xax_lim[1], xaxis_loc - 0.02 * yspan, ax.get_xlabel(), fontsize=14, ha='right', va='top', rotation=12) ax.text(yaxis_loc - 0.02 * xspan, yax_lim[1], ax.get_ylabel(), fontsize=14, ha='right', va='top', rotation=78) ax.set_xlabel('') ax.set_ylabel('') # Add title ax.text(0.5 * (xax_lim[1] + xax_lim[0]), yax_lim[1], ax.get_title(), ha='center', va='bottom', fontsize=16) ax.set_title('') Nlines = len(ax.lines) lines = [xaxis, yaxis] + [ax.lines.pop(0) for i in range(Nlines)] for line in lines: x, y = line.get_data() x_int, y_int = xkcd_line(x, y, xlim, ylim, mag, f1, f2, f3) # create foreground and background line lw = line.get_linewidth() line.set_linewidth(2 * lw) line.set_data(x_int, y_int) # don't add background line for axes if (line is not xaxis) and (line is not yaxis): line_bg = pl.Line2D(x_int, y_int, color=bgcolor, linewidth=8 * lw) ax.add_line(line_bg) ax.add_line(line) # Draw arrow-heads at the end of axes lines arr1 = 0.03 * np.array([-1, 0, -1]) arr2 = 0.02 * np.array([-1, 0, 1]) arr1[::2] += np.random.normal(0, 0.005, 2) arr2[::2] += np.random.normal(0, 0.005, 2) x, y = xaxis.get_data() if '+' in str(xaxis_arrow): ax.plot(x[-1] + arr1 * xspan * aspect, y[-1] + arr2 * yspan, color='k', lw=2) if '-' in str(xaxis_arrow): ax.plot(x[0] - arr1 * xspan * aspect, y[0] - arr2 * yspan, color='k', lw=2) x, y = yaxis.get_data() if '+' in str(yaxis_arrow): ax.plot(x[-1] + arr2 * xspan * aspect, y[-1] + arr1 * yspan, color='k', lw=2) if '-' in str(yaxis_arrow): ax.plot(x[0] - arr2 * xspan * aspect, y[0] - arr1 * yspan, color='k', lw=2) # Change all the fonts to humor-sans. prop = fm.FontProperties(fname='Humor-Sans.ttf', size=16) for text in ax.texts: text.set_fontproperties(prop) # modify legend leg = ax.get_legend() if leg is not None: leg.set_frame_on(False) for child in leg.get_children(): if isinstance(child, pl.Line2D): x, y = child.get_data() child.set_data(xkcd_line(x, y, mag=10, f1=100, f2=0.001)) child.set_linewidth(2 * child.get_linewidth()) if isinstance(child, pl.Text): child.set_fontproperties(prop) # Set the axis limits ax.set_xlim(xax_lim[0] - 0.1 * xspan, xax_lim[1] + 0.1 * xspan) ax.set_ylim(yax_lim[0] - 0.1 * yspan, yax_lim[1] + 0.1 * yspan) # adjust the axes ax.set_xticks([]) ax.set_yticks([]) if expand_axes: ax.figure.set_facecolor(bgcolor) ax.set_axis_off() ax.set_position([0, 0, 1, 1]) return ax \end{lstlisting} \end{codeinput} \end{codecell} \subsection{Testing it Out} Let's test this out with a simple plot. We'll plot two curves, add some labels, and then call \texttt{XKCDify} on the axis. I think the results are pretty nice! \begin{codecell} \begin{codeinput} \begin{lstlisting} %pylab inline \end{lstlisting} \end{codeinput} \begin{codeoutput} \begin{verbatim} Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline]. For more information, type 'help(pylab)'. \end{verbatim} \end{codeoutput} \end{codecell} \begin{codecell} \begin{codeinput} \begin{lstlisting} np.random.seed(0) ax = pylab.axes() x = np.linspace(0, 10, 100) ax.plot(x, np.sin(x) * np.exp(-0.1 * (x - 5) ** 2), 'b', lw=1, label='damped sine') ax.plot(x, -np.cos(x) * np.exp(-0.1 * (x - 5) ** 2), 'r', lw=1, label='damped cosine') ax.set_title('check it out!') ax.set_xlabel('x label') ax.set_ylabel('y label') ax.legend(loc='lower right') ax.set_xlim(0, 10) ax.set_ylim(-1.0, 1.0) #XKCDify the axes -- this operates in-place XKCDify(ax, xaxis_loc=0.0, yaxis_loc=1.0, xaxis_arrow='+-', yaxis_arrow='+-', expand_axes=True) \end{lstlisting} \end{codeinput} \begin{codeoutput} \begin{verbatim} \end{verbatim} \begin{center} \includegraphics[width=0.7\textwidth]{XKCD_plots_orig_files/XKCD_plots_orig_fig_00.png} \par \end{center} \end{codeoutput} \end{codecell} \subsection{Duplicating an XKCD Comic} Now let's see if we can use this to replicated an XKCD comic in matplotlib. This is a good one: \begin{codecell} \begin{codeinput} \begin{lstlisting} Image('http://imgs.xkcd.com/comics/front_door.png') \end{lstlisting} \end{codeinput} \begin{codeoutput} \begin{verbatim} \end{verbatim} \end{codeoutput} \end{codecell} With the new \texttt{XKCDify} function, this is relatively easy to replicate. The results are not exactly identical, but I think it definitely gets the point across! \begin{codecell} \begin{codeinput} \begin{lstlisting} # Some helper functions def norm(x, x0, sigma): return np.exp(-0.5 * (x - x0) ** 2 / sigma ** 2) def sigmoid(x, x0, alpha): return 1. / (1. + np.exp(- (x - x0) / alpha)) # define the curves x = np.linspace(0, 1, 100) y1 = np.sqrt(norm(x, 0.7, 0.05)) + 0.2 * (1.5 - sigmoid(x, 0.8, 0.05)) y2 = 0.2 * norm(x, 0.5, 0.2) + np.sqrt(norm(x, 0.6, 0.05)) + 0.1 * (1 - sigmoid(x, 0.75, 0.05)) y3 = 0.05 + 1.4 * norm(x, 0.85, 0.08) y3[x > 0.85] = 0.05 + 1.4 * norm(x[x > 0.85], 0.85, 0.3) # draw the curves ax = pl.axes() ax.plot(x, y1, c='gray') ax.plot(x, y2, c='blue') ax.plot(x, y3, c='red') ax.text(0.3, -0.1, "Yard") ax.text(0.5, -0.1, "Steps") ax.text(0.7, -0.1, "Door") ax.text(0.9, -0.1, "Inside") ax.text(0.05, 1.1, "fear that\nthere's\nsomething\nbehind me") ax.plot([0.15, 0.2], [1.0, 0.2], '-k', lw=0.5) ax.text(0.25, 0.8, "forward\nspeed") ax.plot([0.32, 0.35], [0.75, 0.35], '-k', lw=0.5) ax.text(0.9, 0.4, "embarrassment") ax.plot([1.0, 0.8], [0.55, 1.05], '-k', lw=0.5) ax.set_title("Walking back to my\nfront door at night:") ax.set_xlim(0, 1) ax.set_ylim(0, 1.5) # modify all the axes elements in-place XKCDify(ax, expand_axes=True) \end{lstlisting} \end{codeinput} \begin{codeoutput} \begin{verbatim} \end{verbatim} \begin{center} \includegraphics[width=0.7\textwidth]{XKCD_plots_orig_files/XKCD_plots_orig_fig_01.png} \par \end{center} \end{codeoutput} \end{codecell} Pretty good for a couple hours's work! I think the possibilities here are pretty limitless: this is going to be a hugely useful and popular feature in matplotlib, especially when the sketch artist PR is mature and part of the main package. I imagine using this style of plot for schematic figures in presentations where the normal crisp matplotlib lines look a bit too ``scientific''. I'm giving a few talks at the end of the month\ldots{} maybe I'll even use some of this code there. This post was written entirely in an IPython Notebook: the notebook file is available for download \href{http://jakevdp.github.com/downloads/notebooks/XKCD\_plots.ipynb}{here}. For more information on blogging with notebooks in octopress, see my \href{http://jakevdp.github.com/blog/2012/10/04/blogging-with-ipython/}{previous post} on the subject. \end{document}