Wow, lots of food for thought.  Thanks John!

On Jul 19, 2007, at 12:18 PM, John Hunter wrote:

= Objects that talk to the backend "primitives" =

Have just a few, fairly rich obects, that the backends need to
understand.  Clear candidates are a Path, Text and Image, but despite
their names, don't confuse these with the eponymous matplotlib
matplotlib Artists, which are higher level than what I'm thinking of
here (eg matplotlib.text.Text does *a lot* of layout, and this would
be offloaded ot the backend in this conception of the Text primitive).

This sounds like a great idea. I think you should consider making transformations a first-class citizen as well, to improve code readability. Some of us have to think really hard to translate "[[width, 0, 0], [0, -height, height], [0, 0, 1]]" into English! ;-)

= Where do the plot functions live? =

In matplotlib, the plot functions are matplotlib.axes.Axes methods and
I think there is consensus that this is a poor design.  Where should
these live, what should they create, etc?

I propose that the plot functions be replaced by plot classes that implement whatever the new Artist API ends up being. I'm not sure where I'd put them (grouped into modules by type?) or how they should be added to an Axes (add_artist()?).

= How much of an intermediate artist layer do we need? =

Do we want to create high level objects like Circle, Rectangle and
Line, each of which manage a Path object under the hood?

Yes, but I don't think they should be subclassed from Path. I found that rather confusing because I read that as "Rectangle is-a Path".

= Z-ordering, containers, etc =

Peter has been doing a lot of nice work on z-order and layers for
chaco, stuff that looks really useful for picking, interaction, etc...
We should look at this approach, and think carefully about how this
should be handled.

Is there somewhere in particular that I can look to see what Peter's been working on? Enthought's svn repositories?

I also plan to use the SWIG agg wrapper, so this gets rid of _backend_agg. If we can enhance the SWIG agg wrapper, we can also do images through there, getting rid of _image.cpp. Having a fully featured, python-exposed agg wrapper will be a plus in mpl and beyond. But with the agg license change, I'm open to discussion of other approaches.

Have you looked into Fredrik Lundh's aggdraw module? Apart from not yet supporting image drawing/blitting, I think it might do the trick. I've attached an aggdraw version your mpl1.py that I wrote as proof of concept. I opted to start hacking instead of installing the traits package, so there's just a basic demo of the renderer for now.

Since aggdraw can work natively with the Python Imaging Library we'd be able to support every raster format under the sun *and* save those images to Python strings. That would make the webapps people very happy.

I want to do away with *all* GUI extension code.

Hurrah!

This means someone needs to figure out how to get TkInter talking to a python
buffer object or a numpy array.

I think PIL's ImageTk module would do the trick for converting RGBA - > PIL Image -> Tk Bitmap/PhotoImage.

= Traits =

I think we should make a major committment to traits and use them from
the ground up.  Even without the UI stuff, they add plenty to make
them worthwhile, especially the validation and notification features.

I hate to be the first one to disagree, but here goes: traits give me the heebie-jeebies. I agree that matplotlib 1.0/2.0 needs to validate all user-settable parameters. However, I'm concerned about the development overhead that might result from making traits as a core dependency. Code readability is also a concern to me -- the experience of reading mpl1.py suggests to me that newcomers might find traits a bit too "voodoo". I'm confident that the same thing could be achieved using Python properties to validate attributes. Change notification is another matter, granted, but I think that a major rewrite will provide the opportunity to better design for those situations.

Ken
import aggdraw
import Image
import numpy as npy


class Renderer:
    def __init__(self, width, height):
        self.width, self.height = width, height

        # almost all renderers assume 0,0 is left, upper, so we'll flip y here by default
        self.affinerenderer  = npy.array(
            [[width, 0, 0], [0, -height, height], [0, 0, 1]], dtype=npy.float_)
        self.pathd = dict() # dict mapping path id -> path instance

    def add_path(self, pathid, path):
        self.pathd[pathid] = path

    def remove_path(self, pathid):
        if pathid in self.pathd:
            del self.pathd[pathid]

    def render_path(self, pathid):
        pass
            


class RendererAggDrawPIL(Renderer):
    def __init__(self, width, height):
        Renderer.__init__(self, width, height)
        # XXX: this flips the origin without scaling to 0..1
        self.affinerenderer  = npy.array(
            [[1,0,0],[0,-1,height],[0,0,1]], dtype=npy.float_)

        self.image = Image.new('RGBA', (width, height), 'white')
        self.draw = aggdraw.Draw(self.image)

    def add_path(self, pathid, path):
        self.pathd[pathid] = path

    def render_path(self, pathid):
        path = self.pathd[pathid]

        self.draw.setantialias(path.antialiased)

        affine = npy.dot(self.affinerenderer, path.affine)
        a, b, tx = affine[0]
        c, d, ty = affine[1]
        self.draw.settransform((a, b, tx, c, d, ty))

        if path.fillcolor is None:
            brush = None
        else:
            brush = aggdraw.Brush(path.fillcolor, int(255*path.alpha))

        if path.strokecolor is None:
            pen = None
        else:
            pen = aggdraw.Pen(path.strokecolor, path.linewidth, int(255*path.alpha))

        agg_path = aggdraw.Path()
        codes, verts = path.pathdata
        N = len(codes)
        MOVETO, LINETO, CLOSEPOLY = Path.MOVETO, Path.LINETO, Path.CLOSEPOLY
        for i in range(N):
            x, y = verts[i]
            code = codes[i]
            if code==MOVETO:
                agg_path.moveto(x, y)
            elif code==LINETO:
                agg_path.lineto(x, y)                
            elif code==CLOSEPOLY:
                agg_path.close()
        self.draw.path((0,0), agg_path, pen, brush)

    def show(self):
        self.draw.flush()

        # we'll cheat a little and use PIL to display the image
        import os
        import sys
        if sys.platform == 'darwin':
            # Avoid PIL opening the image as a JPEG in Preview
            fname = os.tmpnam() + '.png'
            self.image.save(fname, 'PNG')
            os.system('open -a Preview %s' % fname)
        else:
            self.image.show()


class Path:
    """
    The path is an object that talks to the backends, and is an
    intermediary between the high level path artists like Line and
    Polygon, and the backend renderer
    """
    MOVETO, LINETO, CLOSEPOLY = range(3)

    strokecolor = 'black'
    fillcolor = 'blue'
    alpha = 1.0
    linewidth = 1.0
    antialiased = True
    pathdata = []
    affine = npy.array([[1,0,0],[0,1,0],[0,0,1]], npy.float_)


def draw_rectangle(path, lbwh):
    l,b,w,h = lbwh
    t = b+h
    r = l+w
    verts = npy.array([(l,b), (l,t), (r, t), (r, b), (0,0)], npy.float_)
    codes = Path.LINETO*npy.ones(5, npy.uint8)
    codes[0] = Path.MOVETO
    codes[-1] = Path.CLOSEPOLY
    path.pathdata = codes, verts


def draw_line(path, x, y, model=None):
    """
    The model is a function taking Nx2->Nx2.  This is where the
    nonlinear transformation can be used
    """
    X = npy.array([x,y]).T
    numrows, numcols = X.shape

    codes = Path.LINETO*npy.ones(numrows, npy.uint8)
    codes[0] = Path.MOVETO

    if model is None:
        verts = X
    else:
        verts = model(X)

    path.pathdata = codes, verts    
    path.fillcolor = None


class Figure:
    def __init__(self):
        self.renderer = None
        self._pathid = 0
        self.pathd = dict()

    def add_path(self, path):
        id_ = self._pathid
        self.pathd[id_] = path
        self._pathid += 1
        return id_

    def remove_path(self, pathid):
        if pathid in self.pathd:        
            del self.pathd[pathid]
        if self.renderer is not None:
            self.renderer.remove_path(pathid)

    def draw(self):
        if self.renderer is None:
            raise RuntimeError('call set_renderer renderer first')

        for pathid in self.pathd:
            renderer.render_path(pathid)

    def set_renderer(self, renderer):
        self.renderer = renderer
        for pathid, path in self.pathd.items():
            renderer.add_path(pathid, path)


##############################################################################

x = npy.arange(32, 640, 32)
y1 = npy.random.randint(0, 480, len(x))

fig = Figure()

if 1:
    line1 = Path()
    draw_line(line1, x, y1)
    fig.add_path(line1)

if 1:
    rect1 = Path()
    rect2 = Path()
    rect1.alpha = rect2.alpha = 0.5
    rect2.fillcolor = 'red'
    draw_rectangle(rect1, [50,50,100,100])
    draw_rectangle(rect2, [100,100,100,100])
    fig.add_path(rect1)
    fig.add_path(rect2)

renderer = RendererAggDrawPIL(640,480)
fig.set_renderer(renderer)
fig.draw()
renderer.show()
-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2005.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-devel

Reply via email to