Some comments/queries about matplotlib

Hi,

First of all let me congratulate you[1] on your work on Matplotlib -- it so
evidently superior to all the other stuff around for python that it might
actually obviate the need for a little "universal" plotting frontend (anyplot
) as matplotlib is the first such package that would deserve to become a
de-facto standard for plotting in python.

Nonetheless, I've already written most of the foundations and I think it might
actually still be useful (currently e.g. matplotlib still is a bit difficult
to install, and whilst the CVS version apparently already has quite good
support for interactive plotting, at least some features such as good 3D
plotting will likely take longer; and matlab, which is fully supported as
backend for anyplot will likely maintain an edge for a number of plotting
tasks for the immediate future)[2].

To come to the point, recently I've invested a little bit of time to add
matplotlib support[3] and whilst I have been generally postively surprised [4]
both by the quality of the output as well as the ease to get things to work,
there are still a few comments and questions I have:

Here are some things that would be nice to see:

    - AFAICT there is no equivalent to matlab's .fig internal format -- this
      is quite a shame, one of the reasons I why I think a fairly simple
      wrapper like anyplot should get one quite a long way is that I'd guess
      that when excellent visual results are actually important then the
      easiest way might well be to create some "templates" with the
      gui/backend specific commands and use them as prototypes/style-sheets.

      It should of course also be possible to query the underlying plotting
      package and use it's unique functionality where required; there are a
      few places in my code that do that for matlab but it's really not needed
      often and thus doesn't hamper migrating to another backend much.

      I'm planning to adding was some 'record'/'playback' functionality with a
      metaclass i.e. some internal anyplot specific save-format that can be
      used to recreate plots with any available backend -- but this is no full
      substitute because it will only (portably) work for things supported by
      all backends.

    - gcf returns a figure handle -- it would be nice if there were some obvious
      way to get back an integer instead -- I haven't found one yet

    - the LaTeX rendering backend unfortunatly doesn't support any accents
      (\hat{a},\bar{x} etc.); also for me the fact that it doesn't accept normal
      text with $math$ inside happens to be quite a bother -- I can't imagine that
      either of these would be hard to fix.

    - colormap is rather limited in matlab compatibility -- it would be at
      least having some of matlab's colormaps around as functions like `jet`.

    - `ishold` and `spy` are amongst the functions that are missing but would
      be quite easy to add -- ishold in particular would be very useful
      (unlike with gcf I quickly found some way to query the hold-state, but
      that interface might change and anyway it's a generally useful
      facility).

    - plot apparently doesn't know about the "*"-marker style -- this looks
      like a mere oversight (I think all the other marker styles that matlab
      has are supported)

    - the plot command is much more restrictive in what it accepts as inputs;
      ``plot(x1,y2,'r', x2,y2, 'b')`` e.g. would be illegal. This isn't much
      of a problem for me since I've written some code to "parse" plot
      arguments, but I think it's a bit inconvenient. For anyplot I've added
      some more options since I for example found I mainly wanted to use
      several y-vectors against a single x it is also possible to do something
      like e.g ``plot(y1, 'r', y2, 'g+', y3, ':', x='auto', norm=1,
      log='x')``. OTOH I think it might be best to be conservative about
      adding such features before one has a good idea what common use patterns
      are likely to look like (convenience functions could provide a good
      testbed for such ideas).

    - something like axis('manual') would be nice (maybe under a more
      descriptive name) otherwise it's inconvenient to add additional plots
      into boundaries defined by some original plot that is of main interest.

      Unfortunately axis is one of matlab's slightly more overloaded functions
      -- in many of these cases one would think that python's more flexible
      parameter passing syntax (keywords, inter alia) would allow better
      signatures than what matlab's designers came up with (as far as I can
      see matplotlib already makes quite a lot of use kwargs)

Generally anyplot stays closer to matlab in it's interface, but there
are two commands I found too ugly to follow suit exactly:

- matlab's ``text(x,y,[z], string)`` is ``anyplot.text(string, x,y,[z])``

  Is there already a plan what matplotlib going to do about z coords for text,
  once it supports 3D plots?

- `subplot` is just too bloody awkward -- so I deprecated it and instead
  added a `multi` and `refocus` commands, here are the signatures::

  def multi(self,rows,cols=1):
      """Create a multiplot with specified number of `rows` and `cols`. Use
      `refocus` to access the desired cell. Supersedes `subplot`.

      see: `refocus`
      """

  def refocus(self,M=None,N=None):
      """Change focus to the desired subplot in a figure.

       - ``refocus()``
         focus on next multiplot cell (order: right-to-left,
         top-to-down, 0 index)

       - ``refocus(M)``
         focuses on multiplot cell ``M`` (order: same as above)

       - ``refocus(M,N)``
         focuses on multiplot cell at row ``M``, column ``N``

      see: `multi`
      """

I'd generally like to keep anyplot's interface the same as (or at least a
subset or superset of) matplotlib's where ever it makes sense, I'm not quite
convinced about `text` though (I'm not sure how useful z coords for text
really are, if they aren't used even with 3D plot passing them as a keyword,
unfortunately keywords are used to update the font dict; maybe a better
approach would be to use something like (ignoring details like the ordering of
coords and text for the moment)::

    fontInfo = FontInfo(font='Helvetica', height=13, etc='...')
    text(str1, x1, y1 font=fontInfo(height=10))
    text(str2, x2, y2 font=fontInfo(color='red'))

instead of::

    fontInfo = {'font': 'Helvetica', 'height': 13, etc:'...'}
    text(str1, x1, y1 fontdict=fontInfo, height=10)
    text(str2, x2, y2 fontdict=fontInfo, color='red')

because that doesn't scale so well (one no longer can simply add keywords args
for non font-info stuff).

Here `fontInfo` is some structure type that on call creates an updated *copy*.
I've found this quite useful for other things so I've got an implementation
handy: [5]

I'm also wondering how much the interface has already settled down? BTW, one
thing that would be nice is if any sequence dependent quirks (where
unavoidable) would be the same as in matlab, because that would be quite
difficult to work around in anyplot otherwise (e.g. the order of `title`,
`subplot` and `plot`, IIRC; some like the way plot interacts with logplots and
hold might be easier to work around -- I'm still not sure what the best
behavior ist does matplotlib have any fixed policy here already?).

Cheers,

'as

P.S. I will be off-line for about a month from tomorrow noon onwards, so please
     excuse extreme reply latencies :wink: -- I'm unlikely to read anything for
     a month that posted after the next 10/12hrs.

P.P.S. For those who are curious about anyplot, my imminent long off-line
       period finally gave me the push to try to package up all the
       non-pre-alpha stuff I've created over the years and put it on a web
       page in the hope it might be useful for other. Thus hopefully, by
       tomorrow you anyplot and some other possibly useful stuff like my
       high-level matlab-bridge can be found here:

       http://www.dcs.ex.ac.uk/~aschmolc/

       (In the possibly unlikely event that there's some code that would be
       useful for matplotlib -- don't hestitate to take it, license issues
       should be no problem)

P.P.S. A final note: wouldn't it be nicer to have some python-syntax rc file?
       It can always be a safe subset if there are security concerns, but I'd
       rather not have yet another limited and (difficult to extend)
       config-file syntax.

Footnotes:

[1] you here refers to everyone who helped to rapidly transform matplotlib
     into a the to be reckoned with, rather than the zillionth half-arsed and
     bug-ridden plotting package to linger around for years without getting
     anywhere -- first and foremost that credit must of course belong to John
     Hunter.

[2] I started it because I became frustrated with the code disruption caused by
     switching from one unsatisfactory python plotting package to the next I
     started a little, as yet unreleased project some time ago that I dubbed
     anyplot (after the now apparently defunct anygui project). The aim was to
     provide a uniform and (esp. for interactive work) efficient frontend to a few
     different promising backends, because

     a) at the time it wasn't clear on what horse to bet (all of them lamed a bit,
        and the ones that offered most functionality also seemed to suffer from the
        most deeply ingrained problems).

     b) Different plotting packages for python did different things well so it
        seemed useful to easily be able to use specific backend for a certain task
        without having to deal with code changes or memorzing N-interfaces; the
        only one that did everything fairly well was matlab, via the high-level
        python-matlab bridge I wrote, but even matlab often leaves something to be
        desired and I didn't particularly want to be tied to it for eternity. For
        the past 1.5 years or I've almost exclusively used anyplot with matlab as
        backend for all my work, so that part should be fairly stable.

     c) Moreover most had rather painful interfaces -- I wanted something that was
        quick and convenient to use for interactive data exploration. I noticed
        that the vast majority of the time I didn't need fancier capabilities (or
        used the GUI to fine-tune the results), so I reckoned that providing a
        convenient set would be a useful and manageable task, much more so than
        what anypgui set out to do for GUIs. I also took matlab as a point of
        departure, because it is familiar to many people, but also because despite
        some baroqueness the interface is quite quick and efficient for common
        tasks (unlike many of the interfaces that strive for OO-purity).

[3] apart from that, at the moment there's also some xplt support, xplt is
     sufficiently dodgy that I won't invest much further time on this but it
     provides a IMO saner interface to most of the functionality; there's also
     some rather preliminary support for grace, which despite being somewhat
     limited in the type of plots it offers seems to do a few things quite
     well, is very fast and offers a fairly convenient gui; the only thing I'd
     still be interested in additionally supporting would be gnuplot, although
     some of gnuplots weirder limitations (e.g. line styles) would cause some
     awkwardness -- the good thing about gnuplot, though, is that it supports
     both 3D and 2D, zillions of output formats (including more exotic ones
     like ascii, latex and vt100), that it is widespread and runs just about
     on anything and finally that it won't go away anytime soon.

[4] Apart from the installation; I'm a bit spoilt as most libraries I want
     under debian just takes seconds to instal -- I had some trouble with the
     latest sources and eventually downloaded a debian package by hand from
     somewhere because that seemed the only way to get it to work.

[5] Here's the code (obviously not all of this class are would be needed, but
     it should give the idea; also note that it's very convenient to convert
     to/from dicts):

class Struct(object):
    r"""
    Examples:
    
        >>> brian = Struct(name="Brian", age=30)
        >>> brian.name
        'Brian'
        >>> brian.age
        30
        >>> brian.life = "short"
        >>> brian
        Struct(age=30, life='short', name='Brian')
        >>> del brian.life
        >>> brian == Struct(name="Brian", age=30)
        True
        >>> brian != Struct(name="Jesus", age=30)
        True
        >>> len(brian)
        2

    Call the object to create a clone:
    
        >>> brian() is not brian and brian(name="Jesus") == Struct(name="Jesus", age=30)
        True

    Conversion to/from dict:

        >>> Struct(**dict(brian)) == brian
        True

    Evil Stuff:
    
        >>> brian['name', 'age']
        ('Brian', 30)
        >>> brian['name', 'age'] = None, None
        >>> brian
        Struct(age=None, name=None)
    """
    def __init__(self,**kwargs):
        self.__dict__.update(kwargs)
    def __call__(self, **kwargs):
        import copy
        res = copy.copy(self)
        res.__init__(**kwargs)
        return res
    def __eq__(self, other):
        return self.__dict__ == other.__dict__
    def __ne__(self, other):
        return not self.__eq__(other)
    def __len__(self):
        return len([k for k in self.__dict__.iterkeys() if not (k.startswith('__') or k.endswith('__'))])
    # FIXME utterly perverse and UNTESTED
    def __getitem__(self, nameOrNames):
        if isString(nameOrNames):
            return self.__dict__[nameOrNames]
        else:
            return tuple([self.__dict__[n] for n in nameOrNames])
    # FIXME utterly perverse and UNTESTED
    def __setitem__(self, nameOrNames, valueOrValues):
        if isString(nameOrNames):
            self.__dict__[nameOrNames] = valueOrValues
        else:
            for (n,v) in zip(nameOrNames, valueOrValues):
                self.__dict__[n] = v
    def __contains__(self, key):
        return key in self.__dict__ and not (key.startswith('__') or key.endswith('__'))
    def __iter__(self):
        for (k,v) in self.__dict__.iteritems():
            if not (k.startswith('__') or k.endswith('__')):
                yield k,v
    def __repr__(self):
        return mkRepr(self, **vars(self))

def mkRepr(instance, *argls, **kwargs):
    width=79
    maxIndent=15
    minIndent=2
    args = map(repr, argls) + ["%s=%r" % (k, v)
                               for (k,v) in ipsort(kwargs.items())]
    if instance is not None:
        start = "%s(" % instance.__class__.__name__
        args[-1] += ")"
    else:
        start = ""
    if len(start) <= maxIndent and len(start) + len(args[0]) <= width and \
           max(map(len,args)) <= width: # XXX mag of last condition bit arbitrary
        indent = len(start)
        args[0] = start + args[0]
        if sum(map(len, args)) + 2*(len(args) - 1) <= width:
            return ", ".join(args)
    else:
        indent = minIndent
        args[0] = start + "\n" + " " * indent + args[0]
    return (",\n" + " " * indent).join(args)