Matplotlib and IPython love child
This is an experiement of how one could try to improve matplotlib configuration system using IPython's traitlets system; It has probably a huge number of disatvantages going from slowing down all the matplotlib stack, as making every class name matter in backward compatibility.
Far from beeing a perfect lovechild of two awesome project, this is for now an horible mutant that at some point inspect up it's statck to find information about it's caller in some places. It woudl need a fair amount of work to be nicely integrated into matplotlib.
Warning¶
This post has been written with a patched version of Matplotlib, so you will not be able to reproduce this post by re-executing this notebook.
I've also ripped out part of IPython configurable sytem into a small self contained package that you would need.
TL,DR;¶
Here is an example where the color of any Text
object of matplotlib has a configurable color as long as the object that creates it is (also) an Artist
, with minimal modification of matplotlib.
%pylab inline
from IPConfigurable.configurable import Config
matplotlib.rc('font',size=20)
Here is the interesting part where one cas see that everything is magically configurable.
matplotlib.config = Config()
# by default all Text are now purple
matplotlib.config.Text.t_color = 'purple'
# Except Text created by X/Y Axes will red/aqua
matplotlib.config.YAxis.Text.t_color='red'
matplotlib.config.XAxis.Text.t_color='aqua'
# If this is the text of a Tick it should be orange
matplotlib.config.Tick.Text.t_color='orange'
# unless this is an XTick, then it shoudl be Gray-ish
# as (XTick <: Tick) it will have precedence over Tick
matplotlib.config.XTick.Text.t_color=(0.4,0.4,0.3)
## legend
matplotlib.config.TextArea.Text.t_color='y'
matplotlib.config.AxesSubplot.Text.t_color='pink'
plt.plot(sinc(arange(0,3*np.pi,0.1)),'green', label='a sinc')
plt.ylabel('sinc(x)')
plt.xlabel('This is X')
plt.title('Title')
plt.annotate('Max',(20,0.2))
plt.legend()
I love Matplotlib¶
I love Matplotlib and what you can do with it, I am always impressed by how Jake Van Der Plas is able to bend Matpltolib in dooing amazing things. That beeing said, default color for matplotlib graphics are not that nice, mainly because of legacy, and even if Matplotlib is hightly configurable, many libraries are trying to fix it and receipt on the net are common.
IPython configuration is magic¶
I'm not that familiar with Matplotlib internal, but I'm quite familiar with IPython's internal. In particular, using a lightweight version of Enthought Traits we call Traitlets, almost every pieces of IPython is configurable.
According to Clarke's third law
Any sufficiently advanced technology is indistinguishable from magic.
So I'll assume that IPython configuration system is magic. Still there is some rule you shoudl know.
In IPython, any object that inherit from Configurable
can have attributes that are configurable.
The name of the configuration attribute that will allow to change the value of this attribute are easy, it's
Class.attribute = Value
, and if the creator of an object took care of passing a reference to itself, you can nest config as ParentClass.Class.attribute = value
, by dooing so only Class
created by ParentClass
will have value
set. With a dummy example.
class Foo(Configurable):
length = Integer(1,config=True)
...
class Bar(Configurable):
def __init__(self):
foo = Foo(parent=self)
class Rem(Bar):
pass
every Foo
object length can be configured with Foo.length=2
or you can target a subset of foo by setting Rem.Foo.length
or Bar.Foo.lenght
.
But this might be a little abstarct, let's do a demo with matplotlib
cd ~/matplotlib/
let's make matplotlib Artist
an IPython Configurable
, grab default config from matplotlib.config
if it exist, and pass it to parrent.
-class Artist(object):
+class Artist(Configurable):
- def __init__(self):
+ def __init__(self, config=None, parent=None):
+
+ c = getattr(matplotlib,'config',Config({}))
+ if config :
+ c.merge(config)
+ super(Artist, self).__init__(config=c, parent=parent)
Now we will define 2 attributes of Patches
(a subclass of Artist) ; t_color
, t_lw
that are respectively a Color
or a Float
and set the default color of current Patch
to this attribute.
class Patch(artist.Artist):
+ t_color = MaybeColor(None,config=True)
+ t_lw = MaybeFloat(None,config=True)
+
...
if linewidth is None:
- linewidth = mpl.rcParams['patch.linewidth']
+ if self.t_lw is not None:
+ linewidth = self.t_lw
+ else:
+ linewidth = mpl.rcParams['patch.linewidth']
...
if color is None:
- color = mpl.rcParams['patch.facecolor']
+ if self.t_color is not None:
+ color = self.t_color
+ else :
+ color = mpl.rcParams['patch.facecolor']
One could also set _t_color_default
to mpl.rcParams['patch.facecolor']
but it becommes complicaed for the explanation
That's enough¶
This is the minimum viable to have this to work we can know magically configure independently any Subclass of Patche
s
We know that Wedge
, Ellipse
,...
and other are part of this category, so let's play with their t_color
# some minimal imports
import matplotlib.pyplot as plt;
import numpy as np
import matplotlib.path as mpath
import matplotlib.lines as mlines
import matplotlib.patches as mpatches
from matplotlib.collections import PatchCollection
matplotlib.config = Config({
"Wedge" :{"t_color":"0.4"},
"Ellipse" :{"t_color":(0.9, 0.3, 0.7)},
"Circle" :{"t_color":'red'},
"Arrow" :{"t_color":'green'},
"RegularPolygon":{"t_color":'aqua'},
"FancyBboxPatch":{"t_color":'y'},
})
Let's see what this gives :
"""
example derived from
http://matplotlib.org/examples/shapes_and_collections/artist_reference.html
"""
fig, ax = plt.subplots()
grid = np.mgrid[0.2:0.8:3j, 0.2:0.8:3j].reshape(2, -1).T
patches = []
patches.append(mpatches.Circle(grid[0], 0.1,ec="none"))
patches.append(mpatches.Rectangle(grid[1] - [0.025, 0.05], 0.05, 0.1, ec="none"))
patches.append(mpatches.Wedge(grid[2], 0.1, 30, 270, ec="none"))
patches.append(mpatches.RegularPolygon(grid[3], 5, 0.1))
patches.append(mpatches.Ellipse(grid[4], 0.2, 0.1))
patches.append(mpatches.Arrow(grid[5, 0]-0.05, grid[5, 1]-0.05, 0.1, 0.1, width=0.1))
patches.append(mpatches.FancyBboxPatch(
grid[7] - [0.025, 0.05], 0.05, 0.1,
boxstyle=mpatches.BoxStyle("Round", pad=0.02)))
collection = PatchCollection(patches, match_original=True)
ax.add_collection(collection)
plt.subplots_adjust(left=0, right=1, bottom=0, top=1)
plt.axis('equal')
plt.axis('off')
plt.show()
It works !!! Isn't that great ? Free configuration for all Artists
; of course as long as you don't explicitely set the color, or course.
Let's be ugly.¶
We need slightly more to have nested configuration, each Configurable
have to be passed the parent
keyword,
but Matplotlib is not made to pass the parent
keyword to every Artist it creates, this prevent the use of nested configuration. Still using inspect, we can try to get a handle on the parent, by walking up the stack.
adding the following in Artist
constructor:
import inspect
def __init__(self, config=None, parent=None):
i_parent = inspect.currentframe().f_back.f_back.f_locals.get('self',None)
if (i_parent is not self) and (parent is not i_parent) :
if (isinstance(i_parent,Configurable)):
parent = i_parent
....
let's patch Text
to also accept a t_color
configurable, bacause Text
is a good candidate for nesting configurability:
class Text(Artist):
+ t_color = MaybeColor(None,config=True)
if color is None:
- color = rcParams['text.color']
+ if self.t_color is not None:
+ color = self.t_color
+ else :
+ color = rcParams['text.color']
+ if self.t_color is not None:
+ color = self.t_color
+
if fontproperties is None:
fontproperties = FontProperties()
Now we shoudl be able to make default Text
always purple, nice things about Config
object is that once created they accept acces of any attribute with dot notation.
matplotlib.config.Text.t_color = 'purple'
fig,ax = plt.subplots(1,1)
plt.plot(sinc(arange(0,6,0.1)))
plt.ylabel('SinC(x)')
plt.title('SinC of X')
Ok, not much further than current matplotlib configuratin right ?
We also know that XAxis
and Yaxis
inherit from Axis
which itself inherit from Artist
.
Both are responsible from creating the x- and y-label
matplotlib.config.YAxis.Text.t_color='r'
matplotlib.config.YAxis.Text.t_color='aqua'
same goes for Tick
, XTick
and YTicks
. I can of course set a parameter to the root class:
matplotlib.config.Tick.Text.t_color='orange'
and overwrite it for a specific subclass:
matplotlib.config.XTick.Text.t_color='gray'
fig,ax = plt.subplots(1,1)
plt.plot(sinc(arange(0,6,0.1)))
plt.ylabel('SinC(x)')
plt.title('SinC of X')
This is, as far as I know not possible to do with current matplotlib confuguration system. At least not without adding a rc-param for each and every imaginable combinaison.
What more ?¶
First thing it that this make it trivial for external library to plug into matplotlib configuration system to have their own defaults/configurable defaults.
You also can of course refine configurability by use small no-op class that inherit from base classes and give them meaning. Especially right now, Tick
s are separated in XTick
and YTick
with a major/minor attribute.
They shoudl probably be refactor into MajorTick
/MinorTick
. With that you can mix and match configuration from the most global Axis.Tick.value=...
to the more precise YAxis.MinorTick
.
Let's do an example with a custom artist that create 2 kinds of circles. We'll need a custom no-op class that inherit Circle.
class CenterCircle(mpatches.Circle):
pass
matplotlib.config = Config()
matplotlib.config.Circle.t_color='red'
matplotlib.config.CenterCircle.t_color='aqua'
from IPConfigurable.configurable import Configurable
import math
class MyGenArtist(Configurable):
def n_circle(self, x,y,r,n=3):
pi = math.pi
sin,cos = math.sin, math.cos
l= []
for i in range(n):
l.append(mpatches.Circle( ## here Circle
(x+2*r*cos(i*2*pi/n),y+2*r*sin(i*2*pi/n)),
r,
ec="none",
))
l.append(CenterCircle((x,y),r)) ## Here CenterCircle
return l
fig, ax = plt.subplots()
patches = []
patches.extend(MyGenArtist().n_circle(4,1,0.5))
patches.extend(MyGenArtist().n_circle(2,4,0.5,n=6))
patches.extend(MyGenArtist().n_circle(1,1,0.5,n=5))
collection = PatchCollection(patches, match_original=True)
ax.add_collection(collection)
plt.subplots_adjust(left=0, right=1, bottom=0, top=1)
plt.axis('equal')
plt.axis('off')
What's next ?¶
This configuration system is, of course not limited to Matplotib. But to use it it should probably be better decoupled into a separated package first, independently of IPython.
Also if it is ever accepted into matplotlib, there will still be a need to adapt current mechnisme to work on top of this.
Bonus¶
This patches version of matplotlib keep track of all the Configurable
it discovers while you use it.
Here is a non exaustive list.
from matplotlib import artist
print "-------------------------"
print "Some single configurables"
print "-------------------------"
for k in sorted(artist.Artist.s):
print k
print ""
print "----------------------------------"
print "Some possible nested configurables"
print "----------------------------------;"
for k in sorted(artist.Artist.ps):
print k