#!/usr/bin/env python # coding: utf-8 # 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](https://github.com/Carreau/matplotlib/compare/master...traitlets), 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](https://github.com/Carreau/IPConfigurable) 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. # In[1]: get_ipython().run_line_magic('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. # In[2]: 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' # In[3]: 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](http://jakevdp.github.io/blog/2013/07/10/XKCD-plots-in-matplotlib/), I am always impressed by how [Jake Van Der Plas](http://jakevdp.github.io) 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](http://matplotlib.org/users/customizing.html), many [libraries](https://github.com/mwaskom/seaborn) are trying to [fix it](https://github.com/yhat/ggplot/) 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](https://en.wikipedia.org/wiki/Clarke's_three_laws) # # > 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. # # ```python # # 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 # In[4]: 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` # In[5]: # 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 # In[6]: 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 : # In[7]: """ 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: # # ```python # 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. # In[8]: matplotlib.config.Text.t_color = 'purple' # In[9]: 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 # In[10]: 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: # In[11]: matplotlib.config.Tick.Text.t_color='orange' # and overwrite it for a specific subclass: # In[12]: matplotlib.config.XTick.Text.t_color='gray' # In[13]: 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. # In[14]: class CenterCircle(mpatches.Circle): pass # In[15]: matplotlib.config = Config() matplotlib.config.Circle.t_color='red' matplotlib.config.CenterCircle.t_color='aqua' # In[16]: 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. # In[17]: 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