##// END OF EJS Templates
use new css with old nbconvert html
use new css with old nbconvert html

File last commit:

r9595:c56ac074
r9673:1ce1776b
Show More
XKCD_plots.orig.tex
626 lines | 19.9 KiB | application/x-tex | TexLexer
%% 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}
<IPython.core.display.Image at 0x2fef710>
\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}
<IPython.core.display.Image at 0x2fef0d0>
\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}
<matplotlib.axes.AxesSubplot at 0x2fecbd0>
\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}
<IPython.core.display.Image at 0x2ff4a10>
\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}
<matplotlib.axes.AxesSubplot at 0x2fef210>
\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}