Note: figurefirst is a work in progress and should be considered an alpha version. This means that much of the api, as well as this tutorial is unfinished and likely to change.
In [1]:
%pylab
In [2]:
#%matplotlib inline
%config InlineBackend.figure_format = 'png' #svg
import matplotlib.pyplot as plt # this notebook is for plotting
#import pylab as plt
import numpy as np
import scipy as sp
import figurefirst as fifi
from IPython.display import display,SVG
def kill_spines(ax):
return fifi.mpl_functions.adjust_spines(ax,'none',
spine_locations={},
smart_bounds=True,
xticks=None,
yticks=None,
linewidth=1)
def kill_labels(ax):
#ax = ax['axis']
for tl in ax.get_xticklabels() + ax.get_yticklabels():
tl.set_visible(False)
We generated the figurefirst
library because we found that constructing scientific figures that convey information in a clear, efficient and professional way requires that we pay some attention to details of styling and layout. Although there are a few libraries that seek to improve on the default settings of scientific plotting software, much of this styling is still difficult to specify using text-oriented programing languages, and the process inevitably requires some final adjustment using tools available in vector graphics software such as Inkscape or Adobe Illustrator. The main problem with this workflow is that it is challenging to update data presented in a figure after layout and styling decisions have been made. The figurefirst
library seeks to solve this problem by allowing effort devoted to the raw analysis and raw presentation of data to proceed in parallel and independent to the work styling and formating the figure. The approach we take is to facilitate passing graphical information from the open-standard scalable vector graphics (svg) file format into objects consumable by the open-source matplotlib python plotting library.
With figurefirst
creating a new figure generally involves four steps:
figurefirst
should expose to Python.figurefirst FigureLayout
class.figurefirst
to style and organize the figure.As an example, consider constructing a somewhat complicated five-panel figure with non-uniform axes sizes. The documentation for matplotlib.gridspec
provides one such example:
In [3]:
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize = (7.5,4.0))
def make_ticklabels_invisible(fig):
for i, ax in enumerate(fig.axes):
ax.text(0.5, 0.5, "ax%d" % (i+1), va="center", ha="center")
for tl in ax.get_xticklabels() + ax.get_yticklabels():
tl.set_visible(False)
gs = GridSpec(3, 3)
ax1 = plt.subplot(gs[0, :])
# identical to ax1 = plt.subplot(gs.new_subplotspec((0,0), colspan=3))
ax2 = plt.subplot(gs[1,:-1])
ax3 = plt.subplot(gs[1:, -1])
ax4 = plt.subplot(gs[-1,0])
ax5 = plt.subplot(gs[-1,-2])
plt.suptitle("GridSpec")
make_ticklabels_invisible(plt.gcf())
plt.close('all')
fig.savefig('fiveax_gridspec.svg')
display(SVG('fiveax_gridspec.svg'))
To construct a similar plot in figurefirst, we would use Inkscape to draw five boxes in an svg layout document like the one shown below. This layout document would specify the total dimensions of the figure (7.5 by 4.0 in) as well as the placement and aspect ratio of the axes. Also, rather than specifiy the labels progammatically in python we have included them on a separate layer in svg.
In [4]:
display(SVG('fiveax_layout.svg'))
Note that this is a special .svg file such that the each of these grey boxes has been tagged with some xml that indicates importance to figurefirst
and gives each box a name that will become available in python. We will explain details of this tagging procedure below, however know that we have provided a number of inkscape extensions to streamline the process. You will probably also notice that this layout is a bit different than the output of GridSpec
: ax2-5 are visually offset from ax1, and the label placement is less-ridged. We have done this pointedly to illustrate an advantage of using a layout. Those with experience using vector graphics packages such as Inkscape will know that graphic elements can be positioned very precisely, and the regular structure produced by GridSpec
is easily achieved within the context of a layout. The reverse however -- specifying the less regular placement shown in this layout using just python -- would be a bit more difficult.
The block below shows how we would use this layout to make figures:
In [5]:
reload(fifi)
layout = fifi.FigureLayout('fiveax_layout.svg')
layout.make_mplfigures()
[kill_labels(ax) for ax in layout.axes.values()]
layout.insert_figures()
layout.set_layer_visibility('Layer 1',False)
layout.write_svg('fiveax_test_output.svg')
plt.close('all')
display(SVG('fiveax_test_output.svg'))
First, in line 1, we pass the name of the layout file to the FigureLayout
constructor, this causes figurefirst
to load the svg from the layout file.
In line 2 we call the make_mplfigures()
method. This generates a matplotlib figure and populates the figure with axes as specified by the layout.
After the call to layout.make_mplfigures()
we then have access to a dictionary of matplotlib figures in the axes attribute of the layout object. The keys in this dictionary come from the data we passed in via xml. We can then treat these objects like any other maplotlib axis -- plotting data to them if we wish. Here in line 3 we simply pass them to the kill_labels()
function to remove the tick labels.
When we are done plotting and manipulating the axes objects, in line 4 we send the results back into a target svg layer, merging the graphical elements in the layout with those produced by matplotlib in a virtual svg file stored in memory. Since the results of matplotlib get sent into a separate layer, we can remove or hide unwanted elements from the layout like those grey boxes we used to specify the axes using layout.set_layer_visability('Layer 1',False)
.
Finally, in line 6 we write the results of the virtual, merged svg file to a new svg document 'fiveax_test_output.svg'.
Although most users will find inkscape to be the most convenient way to create layout documents for figurefirst
, this is not a hard-and-fast requirement. Indeed, we chose to use svg as the layout file format because, beyond the fact that svg is based on an open-standard, it is a human readable extension of xml that can be edited using nothing more than a text-editor. Since our inkscpe extensions continue to be a work in progress, and it may not always be possible to use inkscape to construct and edit your layout, it is good to have an understaning of the underlying approch we take to construct a layout for figurefirst
. First, consider the contents of a simple .svg file:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="7.5in"
height="4in"
viewBox="0 0 675.000 360.000"
id="svg2"
version="1.1">
<rect
id="rect4703"
width="615.0"
height="300.0"
x="30.0"
y="30.0"
style="fill:#cccccc;stroke:none;">
</rect>
</svg>
We will explain how to convert this file into a layout, but it is worth pointing out a few features of the svg format itself.
The first line declares the type of xml file. Next, we find the tag specifying the beginning of the svg element which has a number of important attributes. First are a series of standard xml namespace declarations, though you do not need to worry about these declarations, you will need to add a line here to construct a figurefirst
layout. Following the xmlns
attributes are the svg height
and width
attributes. These provide the dimensions of the svg file and your final figure. These can be specified in cm,mm and in. Next is the viewBox
attribute. The viewBox
determines the transformation from the units specified in height
and width
into something known as 'user units'. These user units will be used throught the svg document to specify the height, width and position of the graphical elements. In this example, we have chosen a scaling of 90 user units per inch. This happens to be the si definition for a typseting point, and is the default setting for inkscape, but keep in mind that this is arbitrary, and can be anything. The only restriction that figurefirst
makes is that the aspect ratio of the viewBox
is eqivilent to the aspect ratio given by height
and width
and that the first two elements of the viewBox
are set to 0. In most cases you will not need to worry about this, but is worth mentioning in case you need to troubleshoot.
Within the svg element we find one graphical element, a <rect>
. The attributes of this <rect>
specify width, height, x and y position which we will ultimately use to specifiy the positon and size of an axis in the matplotlib figure. In this case, the rect is 7 2/3 inches wide and 3 1/3 in tall and begins 1/3 of an inch in from the top left of the figure. Additionally, the <rect>
has an xml id (inkscape requires that this be unique throught the document) and a style attribute that can be used to specify (among other things) the stroke and fill of the rectangle. Later we will show you how can pass style data such as this into matplotlib to style the lines and fills within your plots, but for the moment the style string is only important to provide the rect with a stroke or fill so that you man visualize the placement of the axes in your layout.
If we save this as 'min_layout.svg' and render the image using a svg editor or web browser we should should see a nice gray box with a white border around it:
In [6]:
display(SVG('min_svg.svg'))
To make this svg document something we can use with figurefirst
we need to add a few lines to our svg file. First we need to append the namespace declarations to add figurefirst
and inkscape.
<svg
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:figurefirst="http://flyranch.github.io/figurefirst/"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
...
Second, we need to tag the <rect>
to identify it as an axis by adding a figurefirst:axis
element so that it is enclosed between <rect>
and </rect>
.
....
<rect
id="rect4703"
width="615.0"
height="300.0"
x="30.0"
y="30.0"
style="fill:#cccccc;stroke:none;">
<figurefirst:axis
figurefirst:name="axis1"/>
</rect>
...
Note that the figurefirst:axis
has a figurefirst:name
attribute. This will identify the axis for us in python.
Finally, we need to add a target layer for the matplotlib output with the <figurefist:targetlayer>
tag.
...
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Layer 1" >
<figurefirst:targetlayer
figurefirst:name='mpl_layer'/>
</g>
...
Now we can save this as 'min_layout.svg' and use it to plot.
In [7]:
reload(fifi)
layout = fifi.FigureLayout('min_layout.svg')
layout.make_mplfigures()
layout.axes['axis1'].plot([1,3,2,4])
layout.insert_figures()
layout.write_svg('min_test_output.svg')
plt.close('all')
display(SVG('min_test_output.svg'))
In [8]:
reload(fifi)
layout = fifi.FigureLayout('nested_groups_layout.svg')
layout.make_mplfigures()
layout.axes_groups['fig2']['group3']['ax2'].plot([2,3,4])
[l.plot([1,3,2,4]) for l in layout.axes.values()]
cdict = {'r1':0.3,'r2':0.1,'r3':0.9}
for key,value in cdict.items():
hexi = matplotlib.colors.rgb2hex(plt.cm.viridis(value))
layout.svgitems['svggroup'][key].style['fill'] = str(hexi)
layout.apply_svg_attrs()
layout.insert_figures()
layout.write_svg('nested_groups.svg')
plt.close('all')
display(SVG('nested_groups.svg'))
In [9]:
reload(fifi)
layout = fifi.FigureLayout('svgitem_layout.svg')
In [10]:
cdict = {'r1':0.3,'r2':0.1,'r3':0.9}
for key,value in cdict.items():
hexi = matplotlib.colors.rgb2hex(plt.cm.viridis(value))
layout.svgitems['svggroup'][key].style['fill'] = str(hexi)
In [11]:
layout.apply_svg_attrs()
layout.make_mplfigures();plt.close('all')
layout.save('svgitem_testoutput.svg')
In [ ]:
In [ ]:
In [ ]:
In [12]:
inkscape_path = '/Applications/Inkscape.app/Contents/Resources/bin/inkscape'
subprocess.call([inkscape_path, '-z','-D','/Users/psilentp/src.git/figurefirst/examples/svgitem_testoutput.svg',
'--export-png=/Users/psilentp/src.git/figurefirst/examples/image.png'])
In [ ]:
In [ ]:
In [ ]:
You can decorate the svg <figurefirsrt:axis>
tag with mpl.axis
methods. For example to call:
ax.axhspan(100,200,zorder=10,color ='r',alpha = 0.3)
on the axis named frequency.22H05.start
use the following tag:
<figurefirst:axis
figurefirst:name="frequency.22H05.start"
figurefirst:axhspan="100,200,zorder=10,color='r',alpha=.3"/>
The layout.apply_mpl_methods function will then apply the methods passing the value of the svg atribute as arguments to the mpl.axis
method.
In [8]:
#Passing axis methods
import numpy as np
layout = fifi.FigureLayout('axis_methods_layout.svg')
layout.make_mplfigures()
layout.fig.set_facecolor('None')
for mplax in layout.axes.values():
ax = mplax['axis']
ax.plot(np.arange(30),np.random.rand(30),color = 'k')
fifi.mpl_functions.adjust_spines(ax,'none',
spine_locations={},
smart_bounds=True,
xticks=None,
yticks=None,
linewidth=1)
ax.patch.set_facecolor('None')
layout.apply_mpl_methods()
layout.insert_figures('mpl_panel_a')
layout.write_svg('axis_methods_test_output.svg')
plt.close('all')
display(SVG('axis_methods_test_output.svg'))
It is also possible to add figurefirst attributes to groups. Providing the figurefirsrt:groupname = "mygroup" attribute will cause the enclosed figurefirst:axes elements to be added to the layout.axes_groups dictionary keyed by groupname, and then axis name. For instance, if the follwing group exists in svg:
<g
style="display:inline"
transform="matrix(0.88667385,0,0,0.84804291,-1.1136586,117.0766)"
id="g3965-1"
figurefirst:groupname="oval">
<rect
y="34.986671"
x="70.899071"
height="42.857143"
width="594.55908"
id="rect2985-3-0"
style="fill:#008000">
<figurefirst:axis
figurefirst:name="circadian" />
</rect>
</g>
python will expose the axis in the dictionary axis groups attached to the layout keyed by group name and axis name.
layout.axes_groups['oval']['circadian']['axis']
All axes that are not included in a group will be collected into the field
layout.axes_groups['none']
In [10]:
#Group axes example
layout = fifi.FigureLayout('group_axes_layout.svg')
layout.make_mplfigures()
layout.insert_figures()
layout.write_svg('group_axes_test_output.svg')
plt.close('all')
display(SVG('group_axes_test_output.svg'))
In [12]:
#Groups and figures example
layout = fifi.FigureLayout('multi_figures_layout.svg')
mplfig = layout.make_mplfigures()
layout.append_figure_to_layer(layout.figures.values()[0],'mpl_layer_2')
layout.append_figure_to_layer(layout.figures.values()[1],'mpl_layer_3')
layout.append_figure_to_layer(layout.figures.values()[2],'mpl_layer_4')
layout.write_svg('multi_fig_test_output.svg')
plt.close('all')
display(SVG('multi_fig_test_output.svg'))
In [13]:
#this is the layout file
display(SVG('pathspec_layout.svg'))
In [14]:
# we collect some data
groupA_mean = 1.5
groupA_sigma = 1.0
groupB_mean = 0.3
groupB_sigma = 0.6
c1_effect = 0.3
c2_effect = 2.0
c3_effect = 0.0
c4_effect = 0.0
data = dict()
N = 500
T = 1.
Delta = T/N
for group_name,group_mean,group_sigma in zip(['A','B'],
[groupA_mean,groupB_mean],
[groupA_sigma,groupB_sigma]):
data[group_name] = dict()
for cond_name,cond_effect in zip(['cond_1','cond_2','cond_3','cond_4'],
[c1_effect,c2_effect,c3_effect,c4_effect]):
data[group_name][cond_name] = list()
for trial in range(10):
W = np.zeros(N+1)
t = np.linspace(0, T, N+1);
W[:N+1] = cond_effect + group_mean + np.cumsum(np.sqrt(Delta) *
np.random.standard_normal(N+1) *
group_sigma)
t = np.linspace(0, T, N+1);
data[group_name][cond_name].append(W)
In [15]:
#to remove spines
def kill_spines(ax):
return fifi.mpl_functions.adjust_spines(ax,'none',
spine_locations={},
smart_bounds=True,
xticks=None,
yticks=None,
linewidth=1)
In [20]:
#to remove spines
def kill_spines(ax):
return fifi.mpl_functions.adjust_spines(ax,'none',
spine_locations={},
smart_bounds=True,
xticks=None,
yticks=None,
linewidth=1)
## create a layout
layout = fifi.FigureLayout('pathspec_layout.svg')
## make the mpl figure objects
mplfig = layout.make_mplfigures()
## load the line and path specs to get plotting colors and effects
layout.load_pathspecs()
## iterate through what you want to plot and find the needed data,
## not the other way around..
for group_name,group in layout.axes_groups.items():
if not(group_name == 'summary'):
for cond_name,cond_ax in group.items():
group_letter = group_name.split('group')[1]
kwargs = layout.pathspecs['trial_%s'%group_letter].mplkwargs()
cond_ax['axis'].plot(np.array(data[group_letter][cond_name]).T,**kwargs)
kill_spines(cond_ax['axis'])
kwargs = layout.pathspecs['mean_%s'%group_letter].mplkwargs()
cond_ax['axis'].plot(np.mean(np.array(data[group_letter][cond_name]).T,axis = 1)
,**kwargs)
cond_ax['axis'].set_ybound(-2,5)
else:
group_letter = 'A'
kwargs = layout.pathspecs['hist%s'%group_letter].mplkwargs()
for cond_name,cond_ax in group.items():
cond_ax['axis'].hist(np.array(data[group_letter][cond_name]).ravel(),
bins = 50,histtype = 'stepfilled',clip_on = False,**kwargs)
cond_ax['axis'].set_xbound(-2,5)
kill_spines(cond_ax['axis'])
group_letter = 'B'
kwargs = layout.pathspecs['hist%s'%group_letter].mplkwargs()
for cond_name,cond_ax in group.items():
cond_ax['axis'].hist(np.array(data[group_letter][cond_name]).ravel(),
bins =50,histtype = 'stepfilled',clip_on = False,**kwargs)
cond_ax['axis'].set_xbound(-2,5)
kill_spines(cond_ax['axis'])
## insert the figures into the layout and save
layout.insert_figures()
layout.write_svg('pathspec_test_output.svg')
plt.close('all')
display(SVG('pathspec_test_output.svg'))
In [17]:
kwargs
Out[17]: