# -*- encoding: utf-8 -*-
#
#
# Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
# Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
# Copyright (C) 2002-2011 André Wobst <wobsta@users.sourceforge.net>
#
# This file is part of PyX (http://pyx.sourceforge.net/).
#
# PyX is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# PyX is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyX; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA


import math, re, string, warnings
from pyx import canvas, path, trafo, unit
from pyx.graph import style
from pyx.graph.axis import axis, positioner


goldenmean = 0.5 * (math.sqrt(5) + 1)


class styledata:
    """style data storage class

    Instances of this class are used to store data from the styles
    and to pass point data to the styles by instances named privatedata
    and sharedata. sharedata is shared between all the style(s) in use
    by a data instance, while privatedata is private to each style and
    used as a storage place instead of self to prevent side effects when
    using a style several times."""
    pass


class plotitem:

    def __init__(self, graph, data, styles):
        self.data = data
        self.title = data.title

        addstyles = [None]
        while addstyles:
            # add styles to ensure all needs of the given styles
            provided = [] # already provided sharedata variables
            addstyles = [] # a list of style instances to be added in front
            for s in styles:
                for n in s.needsdata:
                    if n not in provided:
                        defaultprovider = style.getdefaultprovider(n)
                        addstyles.append(defaultprovider)
                        provided.extend(defaultprovider.providesdata)
                provided.extend(s.providesdata)
            styles = addstyles + styles

        self.styles = styles
        self.sharedata = styledata()
        self.privatedatalist = [styledata() for s in self.styles]

        # perform setcolumns to all styles
        self.usedcolumnnames = []
        for privatedata, s in zip(self.privatedatalist, self.styles):
            self.usedcolumnnames.extend(s.columnnames(privatedata, self.sharedata, graph, self.data.columnnames))

    def selectstyles(self, graph, selectindex, selecttotal):
        for privatedata, style in zip(self.privatedatalist, self.styles):
            style.selectstyle(privatedata, self.sharedata, graph, selectindex, selecttotal)

    def adjustaxesstatic(self, graph):
        for columnname, data in self.data.columns.items():
            for privatedata, style in zip(self.privatedatalist, self.styles):
                style.adjustaxis(privatedata, self.sharedata, graph, columnname, data)

    def makedynamicdata(self, graph):
        self.dynamiccolumns = self.data.dynamiccolumns(graph)

    def adjustaxesdynamic(self, graph):
        for columnname, data in self.dynamiccolumns.items():
            for privatedata, style in zip(self.privatedatalist, self.styles):
                style.adjustaxis(privatedata, self.sharedata, graph, columnname, data)

    def draw(self, graph):
        for privatedata, style in zip(self.privatedatalist, self.styles):
            style.initdrawpoints(privatedata, self.sharedata, graph)
        point = {}
        useitems = []
        for columnname in self.usedcolumnnames:
            try:
                useitems.append((columnname, self.dynamiccolumns[columnname]))
            except KeyError:
                useitems.append((columnname, self.data.columns[columnname]))
        if not useitems:
            raise ValueError("cannot draw empty data")
        for i in xrange(len(useitems[0][1])):
            for columnname, data in useitems:
                point[columnname] = data[i]
            for privatedata, style in zip(self.privatedatalist, self.styles):
                style.drawpoint(privatedata, self.sharedata, graph, point)
        for privatedata, style in zip(self.privatedatalist, self.styles):
            style.donedrawpoints(privatedata, self.sharedata, graph)

    def key_pt(self, graph, x_pt, y_pt, width_pt, height_pt):
        for privatedata, style in zip(self.privatedatalist, self.styles):
            style.key_pt(privatedata, self.sharedata, graph, x_pt, y_pt, width_pt, height_pt)

    def __getattr__(self, attr):
        # read only access to the styles privatedata
        stylesdata = [getattr(styledata, attr)
                      for styledata in self.privatedatalist
                      if hasattr(styledata, attr)]
        if len(stylesdata) > 1:
            return stylesdata
        elif len(stylesdata) == 1:
            return stylesdata[0]
        raise AttributeError("access to styledata attribute '%s' failed" % attr)


class graph(canvas.canvas):

    def __init__(self):
        canvas.canvas.__init__(self)
        self.axes = {}
        self.plotitems = []
        self.keyitems = []
        self._calls = {}
        self.didranges = 0
        self.didstyles = 0

    def did(self, method, *args, **kwargs):
        if not self._calls.has_key(method):
            self._calls[method] = []
        for callargs in self._calls[method]:
            if callargs == (args, kwargs):
                return 1
        self._calls[method].append((args, kwargs))
        return 0

    def bbox(self):
        self.finish()
        return canvas.canvas.bbox(self)

    def registerPS(self, registry):
        self.finish()
        canvas.canvas.registerPS(self, registry)

    def registerPDF(self, registry):
        self.finish()
        canvas.canvas.registerPDF(self, registry)

    def processPS(self, file, writer, context, registry, bbox):
        self.finish()
        canvas.canvas.processPS(self, file, writer, context, registry, bbox)

    def processPDF(self, file, writer, context, registry, bbox):
        self.finish()
        canvas.canvas.processPDF(self, file, writer, context, registry, bbox)

    def plot(self, data, styles=None, rangewarning=1):
        if self.didranges and rangewarning:
            warnings.warn("axes ranges have already been analysed; no further adjustments will be performed")
        if self.didstyles:
            raise RuntimeError("can't plot further data after dostyles() has been executed")
        singledata = 0
        try:
            for d in data:
                pass
        except:
            usedata = [data]
            singledata = 1
        else:
            usedata = data
        if styles is None:
            for d in usedata:
                if styles is None:
                    styles = d.defaultstyles
                elif styles != d.defaultstyles:
                    raise RuntimeError("defaultstyles differ")
        plotitems = []
        for d in usedata:
            plotitems.append(plotitem(self, d, styles))
        self.plotitems.extend(plotitems)
        if self.didranges:
            for aplotitem in plotitems:
                aplotitem.makedynamicdata(self)
        if singledata:
            return plotitems[0]
        else:
            return plotitems

    def doranges(self):
        if self.did(self.doranges):
            return
        for plotitem in self.plotitems:
            plotitem.adjustaxesstatic(self)
        for plotitem in self.plotitems:
            plotitem.makedynamicdata(self)
        for plotitem in self.plotitems:
            plotitem.adjustaxesdynamic(self)
        self.didranges = 1

    def doaxiscreate(self, axisname):
        if self.did(self.doaxiscreate, axisname):
            return
        self.doaxispositioner(axisname)
        self.axes[axisname].create()

    def dolayout(self):
        raise NotImplementedError

    def dobackground(self):
        pass

    def doaxes(self):
        raise NotImplementedError

    def dostyles(self):
        if self.did(self.dostyles):
            return
        self.dolayout()
        self.dobackground()

        # count the usage of styles and perform selects
        styletotal = {}
        def stylesid(styles):
            return ":".join([str(id(style)) for style in styles])
        for plotitem in self.plotitems:
            try:
                styletotal[stylesid(plotitem.styles)] += 1
            except:
                styletotal[stylesid(plotitem.styles)] = 1
        styleindex = {}
        for plotitem in self.plotitems:
            try:
                styleindex[stylesid(plotitem.styles)] += 1
            except:
                styleindex[stylesid(plotitem.styles)] = 0
            plotitem.selectstyles(self, styleindex[stylesid(plotitem.styles)],
                                        styletotal[stylesid(plotitem.styles)])

        self.didstyles = 1

    def doplotitem(self, plotitem):
        if self.did(self.doplotitem, plotitem):
            return
        self.dostyles()
        plotitem.draw(self)

    def doplot(self):
        for plotitem in self.plotitems:
            self.doplotitem(plotitem)

    def dodata(self):
        warnings.warn("dodata() has been deprecated. Use doplot() instead.")
        self.doplot()

    def dokeyitem(self, plotitem):
        if self.did(self.dokeyitem, plotitem):
            return
        self.dostyles()
        if plotitem.title is not None:
            self.keyitems.append(plotitem)

    def dokey(self):
        raise NotImplementedError

    def finish(self):
        self.dobackground()
        self.doaxes()
        self.doplot()
        self.dokey()


class graphxy(graph):

    def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
                 key=None, backgroundattrs=None, axesdist=0.8*unit.v_cm,
                 xaxisat=None, yaxisat=None, **axes):
        graph.__init__(self)

        self.xpos = xpos
        self.ypos = ypos
        self.xpos_pt = unit.topt(self.xpos)
        self.ypos_pt = unit.topt(self.ypos)
        self.xaxisat = xaxisat
        self.yaxisat = yaxisat
        self.key = key
        self.backgroundattrs = backgroundattrs
        self.axesdist_pt = unit.topt(axesdist)

        self.width = width
        self.height = height
        if width is None:
            if height is None:
                raise ValueError("specify width and/or height")
            else:
                self.width = ratio * self.height
        elif height is None:
            self.height = (1.0/ratio) * self.width
        self.width_pt = unit.topt(self.width)
        self.height_pt = unit.topt(self.height)

        for axisname, aaxis in axes.items():
            if aaxis is not None:
                if not isinstance(aaxis, axis.linkedaxis):
                    self.axes[axisname] = axis.anchoredaxis(aaxis, self.texrunner, axisname)
                else:
                    self.axes[axisname] = aaxis
        for axisname, axisat in [("x", xaxisat), ("y", yaxisat)]:
            okey = axisname + "2"
            if not axes.has_key(axisname):
                if not axes.has_key(okey) or axes[okey] is None:
                    self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.texrunner, axisname)
                    if not axes.has_key(okey):
                        self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
                else:
                    self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
            elif not axes.has_key(okey) and axisat is None:
                self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)

        if self.axes.has_key("x"):
            self.xbasepath = self.axes["x"].basepath
            self.xvbasepath = self.axes["x"].vbasepath
            self.xgridpath = self.axes["x"].gridpath
            self.xtickpoint_pt = self.axes["x"].tickpoint_pt
            self.xtickpoint = self.axes["x"].tickpoint
            self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
            self.xvtickpoint = self.axes["x"].tickpoint
            self.xtickdirection = self.axes["x"].tickdirection
            self.xvtickdirection = self.axes["x"].vtickdirection

        if self.axes.has_key("y"):
            self.ybasepath = self.axes["y"].basepath
            self.yvbasepath = self.axes["y"].vbasepath
            self.ygridpath = self.axes["y"].gridpath
            self.ytickpoint_pt = self.axes["y"].tickpoint_pt
            self.ytickpoint = self.axes["y"].tickpoint
            self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
            self.yvtickpoint = self.axes["y"].vtickpoint
            self.ytickdirection = self.axes["y"].tickdirection
            self.yvtickdirection = self.axes["y"].vtickdirection

        self.axesnames = ([], [])
        for axisname, aaxis in self.axes.items():
            if axisname[0] not in "xy" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
                                                                   axisname[1:] == "1")):
                raise ValueError("invalid axis name")
            if axisname[0] == "x":
                self.axesnames[0].append(axisname)
            else:
                self.axesnames[1].append(axisname)
            aaxis.setcreatecall(self.doaxiscreate, axisname)


    def pos_pt(self, x, y, xaxis=None, yaxis=None):
        if xaxis is None:
            xaxis = self.axes["x"]
        if yaxis is None:
            yaxis = self.axes["y"]
        return (self.xpos_pt + xaxis.convert(x)*self.width_pt,
                self.ypos_pt + yaxis.convert(y)*self.height_pt)

    def pos(self, x, y, xaxis=None, yaxis=None):
        if xaxis is None:
            xaxis = self.axes["x"]
        if yaxis is None:
            yaxis = self.axes["y"]
        return (self.xpos + xaxis.convert(x)*self.width,
                self.ypos + yaxis.convert(y)*self.height)

    def vpos_pt(self, vx, vy):
        return (self.xpos_pt + vx*self.width_pt,
                self.ypos_pt + vy*self.height_pt)

    def vpos(self, vx, vy):
        return (self.xpos + vx*self.width,
                self.ypos + vy*self.height)

    def vzindex(self, vx, vy):
        return 0

    def vangle(self, vx1, vy1, vx2, vy2, vx3, vy3):
        return 1

    def vgeodesic(self, vx1, vy1, vx2, vy2):
        """returns a geodesic path between two points in graph coordinates"""
        return path.line_pt(self.xpos_pt + vx1*self.width_pt,
                            self.ypos_pt + vy1*self.height_pt,
                            self.xpos_pt + vx2*self.width_pt,
                            self.ypos_pt + vy2*self.height_pt)

    def vgeodesic_el(self, vx1, vy1, vx2, vy2):
        """returns a geodesic path element between two points in graph coordinates"""
        return path.lineto_pt(self.xpos_pt + vx2*self.width_pt,
                              self.ypos_pt + vy2*self.height_pt)

    def vcap_pt(self, coordinate, length_pt, vx, vy):
        """returns an error cap path for a given coordinate, lengths and
        point in graph coordinates"""
        if coordinate == 0:
            return path.line_pt(self.xpos_pt + vx*self.width_pt - 0.5*length_pt,
                                self.ypos_pt + vy*self.height_pt,
                                self.xpos_pt + vx*self.width_pt + 0.5*length_pt,
                                self.ypos_pt + vy*self.height_pt)
        elif coordinate == 1:
            return path.line_pt(self.xpos_pt + vx*self.width_pt,
                                self.ypos_pt + vy*self.height_pt - 0.5*length_pt,
                                self.xpos_pt + vx*self.width_pt,
                                self.ypos_pt + vy*self.height_pt + 0.5*length_pt)
        else:
            raise ValueError("direction invalid")

    def xvgridpath(self, vx):
        return path.line_pt(self.xpos_pt + vx*self.width_pt, self.ypos_pt,
                            self.xpos_pt + vx*self.width_pt, self.ypos_pt + self.height_pt)

    def yvgridpath(self, vy):
        return path.line_pt(self.xpos_pt, self.ypos_pt + vy*self.height_pt,
                            self.xpos_pt + self.width_pt, self.ypos_pt + vy*self.height_pt)

    def axistrafo(self, axis, t):
        c = canvas.canvas([t])
        c.insert(axis.canvas)
        axis.canvas = c

    def axisatv(self, axis, v):
        if axis.positioner.fixtickdirection[0]:
            # it is a y-axis
            self.axistrafo(axis, trafo.translate_pt(self.xpos_pt + v*self.width_pt - axis.positioner.x1_pt, 0))
        else:
            # it is an x-axis
            self.axistrafo(axis, trafo.translate_pt(0, self.ypos_pt + v*self.height_pt - axis.positioner.y1_pt))

    def doaxispositioner(self, axisname):
        if self.did(self.doaxispositioner, axisname):
            return
        self.doranges()
        if axisname == "x":
            self.axes["x"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
                                                                   self.xpos_pt + self.width_pt, self.ypos_pt,
                                                                   (0, 1), self.xvgridpath))
        elif axisname == "x2":
            self.axes["x2"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt + self.height_pt,
                                                                    self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
                                                                    (0, -1), self.xvgridpath))
        elif axisname == "y":
            self.axes["y"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
                                                                   self.xpos_pt, self.ypos_pt + self.height_pt,
                                                                   (1, 0), self.yvgridpath))
        elif axisname == "y2":
            self.axes["y2"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt + self.width_pt, self.ypos_pt,
                                                                    self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
                                                                    (-1, 0), self.yvgridpath))
        else:
            if axisname[1:] == "3":
                dependsonaxisname = axisname[0]
            else:
                dependsonaxisname = "%s%d" % (axisname[0], int(axisname[1:]) - 2)
            self.doaxiscreate(dependsonaxisname)
            sign = 2*(int(axisname[1:]) % 2) - 1
            if axisname[0] == "x":
                y_pt = self.axes[dependsonaxisname].positioner.y1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
                self.axes[axisname].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, y_pt,
                                                                            self.xpos_pt + self.width_pt, y_pt,
                                                                            (0, sign), self.xvgridpath))
            else:
                x_pt = self.axes[dependsonaxisname].positioner.x1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
                self.axes[axisname].setpositioner(positioner.lineaxispos_pt(x_pt, self.ypos_pt,
                                                                            x_pt, self.ypos_pt + self.height_pt,
                                                                            (sign, 0), self.yvgridpath))

    def dolayout(self):
        if self.did(self.dolayout):
            return
        for axisname in self.axes.keys():
            self.doaxiscreate(axisname)
        if self.xaxisat is not None:
            self.axisatv(self.axes["x"], self.axes["y"].convert(self.xaxisat))
        if self.yaxisat is not None:
            self.axisatv(self.axes["y"], self.axes["x"].convert(self.yaxisat))

    def dobackground(self):
        if self.did(self.dobackground):
            return
        if self.backgroundattrs is not None:
            self.draw(path.rect_pt(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt),
                      self.backgroundattrs)

    def doaxes(self):
        if self.did(self.doaxes):
            return
        self.dolayout()
        self.dobackground()
        for axis in self.axes.values():
            self.insert(axis.canvas)

    def dokey(self):
        if self.did(self.dokey):
            return
        self.dobackground()
        for plotitem in self.plotitems:
            self.dokeyitem(plotitem)
        if self.key is not None:
            c = self.key.paint(self.keyitems)
            bbox = c.bbox()
            def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
                ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
                cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
                return ppos-cpos
            if bbox:
                x = parentchildalign(self.xpos_pt, self.xpos_pt+self.width_pt,
                                     bbox.llx_pt, bbox.urx_pt,
                                     self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
                y = parentchildalign(self.ypos_pt, self.ypos_pt+self.height_pt,
                                     bbox.lly_pt, bbox.ury_pt,
                                     self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
                self.insert(c, [trafo.translate_pt(x, y)])


class graphxyz(graphxy):

    class central:

        def __init__(self, distance, phi, theta, anglefactor=math.pi/180):
            phi *= anglefactor
            theta *= anglefactor
            self.distance = distance

            self.a = (-math.sin(phi), math.cos(phi), 0)
            self.b = (-math.cos(phi)*math.sin(theta),
                      -math.sin(phi)*math.sin(theta),
                      math.cos(theta))
            self.eye = (distance*math.cos(phi)*math.cos(theta),
                        distance*math.sin(phi)*math.cos(theta),
                        distance*math.sin(theta))

        def point(self, x, y, z):
            d0 = (self.a[0]*self.b[1]*(z-self.eye[2])
                + self.a[2]*self.b[0]*(y-self.eye[1])
                + self.a[1]*self.b[2]*(x-self.eye[0])
                - self.a[2]*self.b[1]*(x-self.eye[0])
                - self.a[0]*self.b[2]*(y-self.eye[1])
                - self.a[1]*self.b[0]*(z-self.eye[2]))
            da = (self.eye[0]*self.b[1]*(z-self.eye[2])
                + self.eye[2]*self.b[0]*(y-self.eye[1])
                + self.eye[1]*self.b[2]*(x-self.eye[0])
                - self.eye[2]*self.b[1]*(x-self.eye[0])
                - self.eye[0]*self.b[2]*(y-self.eye[1])
                - self.eye[1]*self.b[0]*(z-self.eye[2]))
            db = (self.a[0]*self.eye[1]*(z-self.eye[2])
                + self.a[2]*self.eye[0]*(y-self.eye[1])
                + self.a[1]*self.eye[2]*(x-self.eye[0])
                - self.a[2]*self.eye[1]*(x-self.eye[0])
                - self.a[0]*self.eye[2]*(y-self.eye[1])
                - self.a[1]*self.eye[0]*(z-self.eye[2]))
            return da/d0, db/d0

        def zindex(self, x, y, z):
            return math.sqrt((x-self.eye[0])*(x-self.eye[0])+(y-self.eye[1])*(y-self.eye[1])+(z-self.eye[2])*(z-self.eye[2]))-self.distance

        def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
            sx = (x1-self.eye[0])
            sy = (y1-self.eye[1])
            sz = (z1-self.eye[2])
            nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
            ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
            nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
            return (sx*nx+sy*ny+sz*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)/math.sqrt(sx*sx+sy*sy+sz*sz)


    class parallel:

        def __init__(self, phi, theta, anglefactor=math.pi/180):
            phi *= anglefactor
            theta *= anglefactor

            self.a = (-math.sin(phi), math.cos(phi), 0)
            self.b = (-math.cos(phi)*math.sin(theta),
                      -math.sin(phi)*math.sin(theta),
                      math.cos(theta))
            self.c = (-math.cos(phi)*math.cos(theta),
                      -math.sin(phi)*math.cos(theta),
                      -math.sin(theta))

        def point(self, x, y, z):
            return self.a[0]*x+self.a[1]*y+self.a[2]*z, self.b[0]*x+self.b[1]*y+self.b[2]*z

        def zindex(self, x, y, z):
            return self.c[0]*x+self.c[1]*y+self.c[2]*z

        def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
            nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
            ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
            nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
            return (self.c[0]*nx+self.c[1]*ny+self.c[2]*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)


    def __init__(self, xpos=0, ypos=0, size=None,
                 xscale=1, yscale=1, zscale=1/goldenmean,
                 projector=central(10, -30, 30), key=None,
                 **axes):
        graph.__init__(self)

        self.xpos = xpos
        self.ypos = ypos
        self.size = size
        self.xpos_pt = unit.topt(xpos)
        self.ypos_pt = unit.topt(ypos)
        self.size_pt = unit.topt(size)
        self.xscale = xscale
        self.yscale = yscale
        self.zscale = zscale
        self.projector = projector
        self.key = key

        self.xorder = projector.zindex(0, -1, 0) > projector.zindex(0, 1, 0) and 1 or 0
        self.yorder = projector.zindex(-1, 0, 0) > projector.zindex(1, 0, 0) and 1 or 0
        self.zindexscale = math.sqrt(xscale*xscale+yscale*yscale+zscale*zscale)

        for axisname, aaxis in axes.items():
            if aaxis is not None:
                if not isinstance(aaxis, axis.linkedaxis):
                    self.axes[axisname] = axis.anchoredaxis(aaxis, self.texrunner, axisname)
                else:
                    self.axes[axisname] = aaxis
        for axisname in ["x", "y"]:
            okey = axisname + "2"
            if not axes.has_key(axisname):
                if not axes.has_key(okey) or axes[okey] is None:
                    self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.texrunner, axisname)
                    if not axes.has_key(okey):
                        self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
                else:
                    self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
        if not axes.has_key("z"):
            self.axes["z"] = axis.anchoredaxis(axis.linear(), self.texrunner, "z")

        if self.axes.has_key("x"):
            self.xbasepath = self.axes["x"].basepath
            self.xvbasepath = self.axes["x"].vbasepath
            self.xgridpath = self.axes["x"].gridpath
            self.xtickpoint_pt = self.axes["x"].tickpoint_pt
            self.xtickpoint = self.axes["x"].tickpoint
            self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
            self.xvtickpoint = self.axes["x"].tickpoint
            self.xtickdirection = self.axes["x"].tickdirection
            self.xvtickdirection = self.axes["x"].vtickdirection

        if self.axes.has_key("y"):
            self.ybasepath = self.axes["y"].basepath
            self.yvbasepath = self.axes["y"].vbasepath
            self.ygridpath = self.axes["y"].gridpath
            self.ytickpoint_pt = self.axes["y"].tickpoint_pt
            self.ytickpoint = self.axes["y"].tickpoint
            self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
            self.yvtickpoint = self.axes["y"].vtickpoint
            self.ytickdirection = self.axes["y"].tickdirection
            self.yvtickdirection = self.axes["y"].vtickdirection

        if self.axes.has_key("z"):
            self.zbasepath = self.axes["z"].basepath
            self.zvbasepath = self.axes["z"].vbasepath
            self.zgridpath = self.axes["z"].gridpath
            self.ztickpoint_pt = self.axes["z"].tickpoint_pt
            self.ztickpoint = self.axes["z"].tickpoint
            self.zvtickpoint_pt = self.axes["z"].vtickpoint
            self.zvtickpoint = self.axes["z"].vtickpoint
            self.ztickdirection = self.axes["z"].tickdirection
            self.zvtickdirection = self.axes["z"].vtickdirection

        self.axesnames = ([], [], [])
        for axisname, aaxis in self.axes.items():
            if axisname[0] not in "xyz" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
                                                                    axisname[1:] == "1")):
                raise ValueError("invalid axis name")
            if axisname[0] == "x":
                self.axesnames[0].append(axisname)
            elif axisname[0] == "y":
                self.axesnames[1].append(axisname)
            else:
                self.axesnames[2].append(axisname)
            aaxis.setcreatecall(self.doaxiscreate, axisname)

    def pos_pt(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
        if xaxis is None:
            xaxis = self.axes["x"]
        if yaxis is None:
            yaxis = self.axes["y"]
        if zaxis is None:
            zaxis = self.axes["z"]
        return self.vpos_pt(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))

    def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
        if xaxis is None:
            xaxis = self.axes["x"]
        if yaxis is None:
            yaxis = self.axes["y"]
        if zaxis is None:
            zaxis = self.axes["z"]
        return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))

    def vpos_pt(self, vx, vy, vz):
        x, y = self.projector.point(2*self.xscale*(vx - 0.5),
                                    2*self.yscale*(vy - 0.5),
                                    2*self.zscale*(vz - 0.5))
        return self.xpos_pt+x*self.size_pt, self.ypos_pt+y*self.size_pt

    def vpos(self, vx, vy, vz):
        x, y = self.projector.point(2*self.xscale*(vx - 0.5),
                                    2*self.yscale*(vy - 0.5),
                                    2*self.zscale*(vz - 0.5))
        return self.xpos+x*self.size, self.ypos+y*self.size

    def vzindex(self, vx, vy, vz):
        return self.projector.zindex(2*self.xscale*(vx - 0.5),
                                     2*self.yscale*(vy - 0.5),
                                     2*self.zscale*(vz - 0.5))/self.zindexscale

    def vangle(self, vx1, vy1, vz1, vx2, vy2, vz2, vx3, vy3, vz3):
        return self.projector.angle(2*self.xscale*(vx1 - 0.5),
                                    2*self.yscale*(vy1 - 0.5),
                                    2*self.zscale*(vz1 - 0.5),
                                    2*self.xscale*(vx2 - 0.5),
                                    2*self.yscale*(vy2 - 0.5),
                                    2*self.zscale*(vz2 - 0.5),
                                    2*self.xscale*(vx3 - 0.5),
                                    2*self.yscale*(vy3 - 0.5),
                                    2*self.zscale*(vz3 - 0.5))

    def vgeodesic(self, vx1, vy1, vz1, vx2, vy2, vz2):
        """returns a geodesic path between two points in graph coordinates"""
        return path.line_pt(*(self.vpos_pt(vx1, vy1, vz1) + self.vpos_pt(vx2, vy2, vz2)))

    def vgeodesic_el(self, vx1, vy1, vz1, vx2, vy2, vz2):
        """returns a geodesic path element between two points in graph coordinates"""
        return path.lineto_pt(*self.vpos_pt(vx2, vy2, vz2))

    def vcap_pt(self, coordinate, length_pt, vx, vy, vz):
        """returns an error cap path for a given coordinate, lengths and
        point in graph coordinates"""
        if coordinate == 0:
            return self.vgeodesic(vx-0.5*length_pt/self.size_pt, vy, vz, vx+0.5*length_pt/self.size_pt, vy, vz)
        elif coordinate == 1:
            return self.vgeodesic(vx, vy-0.5*length_pt/self.size_pt, vz, vx, vy+0.5*length_pt/self.size_pt, vz)
        elif coordinate == 2:
            return self.vgeodesic(vx, vy, vz-0.5*length_pt/self.size_pt, vx, vy, vz+0.5*length_pt/self.size_pt)
        else:
            raise ValueError("direction invalid")

    def xvtickdirection(self, vx):
        if self.xorder:
            x1_pt, y1_pt = self.vpos_pt(vx, 1, 0)
            x2_pt, y2_pt = self.vpos_pt(vx, 0, 0)
        else:
            x1_pt, y1_pt = self.vpos_pt(vx, 0, 0)
            x2_pt, y2_pt = self.vpos_pt(vx, 1, 0)
        dx_pt = x2_pt - x1_pt
        dy_pt = y2_pt - y1_pt
        norm = math.hypot(dx_pt, dy_pt)
        return dx_pt/norm, dy_pt/norm

    def yvtickdirection(self, vy):
        if self.yorder:
            x1_pt, y1_pt = self.vpos_pt(1, vy, 0)
            x2_pt, y2_pt = self.vpos_pt(0, vy, 0)
        else:
            x1_pt, y1_pt = self.vpos_pt(0, vy, 0)
            x2_pt, y2_pt = self.vpos_pt(1, vy, 0)
        dx_pt = x2_pt - x1_pt
        dy_pt = y2_pt - y1_pt
        norm = math.hypot(dx_pt, dy_pt)
        return dx_pt/norm, dy_pt/norm

    def vtickdirection(self, vx1, vy1, vz1, vx2, vy2, vz2):
        x1_pt, y1_pt = self.vpos_pt(vx1, vy1, vz1)
        x2_pt, y2_pt = self.vpos_pt(vx2, vy2, vz2)
        dx_pt = x2_pt - x1_pt
        dy_pt = y2_pt - y1_pt
        norm = math.hypot(dx_pt, dy_pt)
        return dx_pt/norm, dy_pt/norm

    def xvgridpath(self, vx):
        return path.path(path.moveto_pt(*self.vpos_pt(vx, 0, 0)),
                         path.lineto_pt(*self.vpos_pt(vx, 1, 0)),
                         path.lineto_pt(*self.vpos_pt(vx, 1, 1)),
                         path.lineto_pt(*self.vpos_pt(vx, 0, 1)),
                         path.closepath())

    def yvgridpath(self, vy):
        return path.path(path.moveto_pt(*self.vpos_pt(0, vy, 0)),
                         path.lineto_pt(*self.vpos_pt(1, vy, 0)),
                         path.lineto_pt(*self.vpos_pt(1, vy, 1)),
                         path.lineto_pt(*self.vpos_pt(0, vy, 1)),
                         path.closepath())

    def zvgridpath(self, vz):
        return path.path(path.moveto_pt(*self.vpos_pt(0, 0, vz)),
                         path.lineto_pt(*self.vpos_pt(1, 0, vz)),
                         path.lineto_pt(*self.vpos_pt(1, 1, vz)),
                         path.lineto_pt(*self.vpos_pt(0, 1, vz)),
                         path.closepath())

    def doaxispositioner(self, axisname):
        if self.did(self.doaxispositioner, axisname):
            return
        self.doranges()
        if axisname == "x":
            self.axes["x"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 0),
                                                                       lambda vx: self.vtickdirection(vx, self.xorder, 0, vx, 1-self.xorder, 0),
                                                                       self.xvgridpath))
        elif axisname == "x2":
            self.axes["x2"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 0),
                                                                        lambda vx: self.vtickdirection(vx, 1-self.xorder, 0, vx, self.xorder, 0),
                                                                        self.xvgridpath))
        elif axisname == "x3":
            self.axes["x3"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 1),
                                                                        lambda vx: self.vtickdirection(vx, self.xorder, 1, vx, 1-self.xorder, 1),
                                                                        self.xvgridpath))
        elif axisname == "x4":
            self.axes["x4"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 1),
                                                                        lambda vx: self.vtickdirection(vx, 1-self.xorder, 1, vx, self.xorder, 1),
                                                                        self.xvgridpath))
        elif axisname == "y":
            self.axes["y"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 0),
                                                                       lambda vy: self.vtickdirection(self.yorder, vy, 0, 1-self.yorder, vy, 0),
                                                                       self.yvgridpath))
        elif axisname == "y2":
            self.axes["y2"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 0),
                                                                       lambda vy: self.vtickdirection(1-self.yorder, vy, 0, self.yorder, vy, 0),
                                                                       self.yvgridpath))
        elif axisname == "y3":
            self.axes["y3"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 1),
                                                                       lambda vy: self.vtickdirection(self.yorder, vy, 1, 1-self.yorder, vy, 1),
                                                                       self.yvgridpath))
        elif axisname == "y4":
            self.axes["y4"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 1),
                                                                       lambda vy: self.vtickdirection(1-self.yorder, vy, 1, self.yorder, vy, 1),
                                                                       self.yvgridpath))
        elif axisname == "z":
            self.axes["z"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 0, vz),
                                                                       lambda vz: self.vtickdirection(0, 0, vz, 1, 1, vz),
                                                                       self.zvgridpath))
        elif axisname == "z2":
            self.axes["z2"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 0, vz),
                                                                       lambda vz: self.vtickdirection(1, 0, vz, 0, 1, vz),
                                                                       self.zvgridpath))
        elif axisname == "z3":
            self.axes["z3"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 1, vz),
                                                                       lambda vz: self.vtickdirection(0, 1, vz, 1, 0, vz),
                                                                       self.zvgridpath))
        elif axisname == "z4":
            self.axes["z4"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 1, vz),
                                                                       lambda vz: self.vtickdirection(1, 1, vz, 0, 0, vz),
                                                                       self.zvgridpath))
        else:
            raise NotImplementedError("4 axis per dimension supported only")

    def dolayout(self):
        if self.did(self.dolayout):
            return
        for axisname in self.axes.keys():
            self.doaxiscreate(axisname)

    def dobackground(self):
        if self.did(self.dobackground):
            return

    def doaxes(self):
        if self.did(self.doaxes):
            return
        self.dolayout()
        self.dobackground()
        for axis in self.axes.values():
            self.insert(axis.canvas)

    def dokey(self):
        if self.did(self.dokey):
            return
        self.dobackground()
        for plotitem in self.plotitems:
            self.dokeyitem(plotitem)
        if self.key is not None:
            c = self.key.paint(self.keyitems)
            bbox = c.bbox()
            def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
                ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
                cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
                return ppos-cpos
            if bbox:
                x = parentchildalign(self.xpos_pt, self.xpos_pt+self.size_pt,
                                     bbox.llx_pt, bbox.urx_pt,
                                     self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
                y = parentchildalign(self.ypos_pt, self.ypos_pt+self.size_pt,
                                     bbox.lly_pt, bbox.ury_pt,
                                     self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
                self.insert(c, [trafo.translate_pt(x, y)])
