1# -*- encoding: utf-8 -*-
2#
3#
4# Copyright (C) 2002-2012 Jörg Lehmann <joerg@pyx-project.org>
5# Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6# Copyright (C) 2002-2012 André Wobst <wobsta@pyx-project.org>
7#
8# This file is part of PyX (https://pyx-project.org/).
9#
10# PyX is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation; either version 2 of the License, or
13# (at your option) any later version.
14#
15# PyX is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with PyX; if not, write to the Free Software
22# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
23
24
25import logging, math, re, string
26from pyx import canvas, path, trafo, unit
27from pyx.graph.axis import axis, positioner
28
29logger = logging.getLogger("pyx")
30goldenmean = 0.5 * (math.sqrt(5) + 1)
31
32
33# The following two methods are used to register and get a default provider
34# for keys. A key is a variable name in sharedata. A provider is a style
35# which creates variables in sharedata.
36
37_defaultprovider = {}
38
39def registerdefaultprovider(style, keys):
40    """sets a style as a default creator for sharedata variables 'keys'"""
41    for key in keys:
42        assert key in style.providesdata, "key not provided by style"
43        # we might allow for overwriting the defaults, i.e. the following is not checked:
44        # assert key in _defaultprovider.keys(), "default provider already registered for key"
45        _defaultprovider[key] = style
46
47def getdefaultprovider(key):
48    """returns a style, which acts as a default creator for the
49    sharedata variable 'key'"""
50    return _defaultprovider[key]
51
52
53class styledata:
54    """style data storage class
55
56    Instances of this class are used to store data from the styles
57    and to pass point data to the styles by instances named privatedata
58    and sharedata. sharedata is shared between all the style(s) in use
59    by a data instance, while privatedata is private to each style and
60    used as a storage place instead of self to prevent side effects when
61    using a style several times."""
62    pass
63
64
65class plotitem:
66
67    def __init__(self, graph, data, styles):
68        self.data = data
69        self.title = data.title
70
71        addstyles = [None]
72        while addstyles:
73            # add styles to ensure all needs of the given styles
74            provided = [] # already provided sharedata variables
75            addstyles = [] # a list of style instances to be added in front
76            for s in styles:
77                for n in s.needsdata:
78                    if n not in provided:
79                        defaultprovider = getdefaultprovider(n)
80                        addstyles.append(defaultprovider)
81                        provided.extend(defaultprovider.providesdata)
82                provided.extend(s.providesdata)
83            styles = addstyles + styles
84
85        self.styles = styles
86        self.sharedata = styledata()
87        self.dataaxisnames = {}
88        self.privatedatalist = [styledata() for s in self.styles]
89
90        # perform setcolumns to all styles
91        self.usedcolumnnames = set()
92        for privatedata, s in zip(self.privatedatalist, self.styles):
93            self.usedcolumnnames.update(set(s.columnnames(privatedata, self.sharedata, graph, self.data.columnnames, self.dataaxisnames)))
94
95    def selectstyles(self, graph, selectindex, selecttotal):
96        for privatedata, style in zip(self.privatedatalist, self.styles):
97            style.selectstyle(privatedata, self.sharedata, graph, selectindex, selecttotal)
98
99    def adjustaxesstatic(self, graph):
100        for columnname, data in list(self.data.columns.items()):
101            for privatedata, style in zip(self.privatedatalist, self.styles):
102                style.adjustaxis(privatedata, self.sharedata, graph, self, columnname, data)
103
104    def makedynamicdata(self, graph):
105        self.dynamiccolumns = self.data.dynamiccolumns(graph, self.dataaxisnames)
106
107    def adjustaxesdynamic(self, graph):
108        for columnname, data in list(self.dynamiccolumns.items()):
109            for privatedata, style in zip(self.privatedatalist, self.styles):
110                style.adjustaxis(privatedata, self.sharedata, graph, self, columnname, data)
111
112    def draw(self, graph):
113        for privatedata, style in zip(self.privatedatalist, self.styles):
114            style.initdrawpoints(privatedata, self.sharedata, graph)
115
116        point = dict([(columnname, None) for columnname in self.usedcolumnnames])
117        # fill point with (static) column data first
118        columns = list(self.data.columns.keys())
119        for values in zip(*list(self.data.columns.values())):
120            for column, value in zip(columns, values):
121                point[column] = value
122            for privatedata, style in zip(self.privatedatalist, self.styles):
123                style.drawpoint(privatedata, self.sharedata, graph, point)
124
125        point = dict([(columnname, None) for columnname in self.usedcolumnnames])
126        # insert an empty point
127        if self.data.columns and self.dynamiccolumns:
128            for privatedata, style in zip(self.privatedatalist, self.styles):
129                style.drawpoint(privatedata, self.sharedata, graph, point)
130        # fill point with dynamic column data
131        columns = list(self.dynamiccolumns.keys())
132        for values in zip(*list(self.dynamiccolumns.values())):
133            for key, value in zip(columns, values):
134                point[key] = value
135            for privatedata, style in zip(self.privatedatalist, self.styles):
136                style.drawpoint(privatedata, self.sharedata, graph, point)
137        for privatedata, style in zip(self.privatedatalist, self.styles):
138            style.donedrawpoints(privatedata, self.sharedata, graph)
139
140    def key_pt(self, graph, x_pt, y_pt, width_pt, height_pt):
141        for privatedata, style in zip(self.privatedatalist, self.styles):
142            style.key_pt(privatedata, self.sharedata, graph, x_pt, y_pt, width_pt, height_pt)
143
144    def __getattr__(self, attr):
145        # read only access to the styles privatedata
146        # this is just a convenience method
147        # use case: access the path of a the line style
148        stylesdata = [getattr(styledata, attr)
149                      for styledata in self.privatedatalist
150                      if hasattr(styledata, attr)]
151        if len(stylesdata) > 1:
152            return stylesdata
153        elif len(stylesdata) == 1:
154            return stylesdata[0]
155        raise AttributeError("access to styledata attribute '%s' failed" % attr)
156
157
158class graph(canvas.canvas):
159
160    def __init__(self):
161        canvas.canvas.__init__(self)
162        for name in ["background", "filldata", "axes.grid", "axes.baseline", "axes.ticks", "axes.labels", "axes.title", "data", "key"]:
163            self.layer(name)
164        self.axes = {}
165        self.plotitems = []
166        self.keyitems = []
167        self._calls = {}
168        self.didranges = 0
169        self.didstyles = 0
170
171    def did(self, method, *args, **kwargs):
172        if method not in self._calls:
173            self._calls[method] = []
174        for callargs in self._calls[method]:
175            if callargs == (args, kwargs):
176                return 1
177        self._calls[method].append((args, kwargs))
178        return 0
179
180    def bbox(self):
181        self.finish()
182        return canvas.canvas.bbox(self)
183
184
185    def processPS(self, file, writer, context, registry, bbox):
186        self.finish()
187        canvas.canvas.processPS(self, file, writer, context, registry, bbox)
188
189    def processPDF(self, file, writer, context, registry, bbox):
190        self.finish()
191        canvas.canvas.processPDF(self, file, writer, context, registry, bbox)
192
193    def plot(self, data, styles=None, rangewarning=1):
194        if self.didranges and rangewarning:
195            logger.warning("axes ranges have already been analysed; no further adjustments will be performed")
196        if self.didstyles:
197            raise RuntimeError("can't plot further data after dostyles() has been executed")
198        singledata = 0
199        try:
200            for d in data:
201                pass
202        except:
203            usedata = [data]
204            singledata = 1
205        else:
206            usedata = data
207        if styles is None:
208            for d in usedata:
209                if styles is None:
210                    styles = d.defaultstyles
211                elif styles != d.defaultstyles:
212                    raise RuntimeError("defaultstyles differ")
213        plotitems = []
214        for d in usedata:
215            plotitems.append(plotitem(self, d, styles))
216        self.plotitems.extend(plotitems)
217        if self.didranges:
218            for aplotitem in plotitems:
219                aplotitem.makedynamicdata(self)
220        if singledata:
221            return plotitems[0]
222        else:
223            return plotitems
224
225    def doranges(self):
226        if self.did(self.doranges):
227            return
228        for plotitem in self.plotitems:
229            plotitem.adjustaxesstatic(self)
230        for plotitem in self.plotitems:
231            plotitem.makedynamicdata(self)
232        for plotitem in self.plotitems:
233            plotitem.adjustaxesdynamic(self)
234        self.didranges = 1
235
236    def doaxiscreate(self, axisname):
237        if self.did(self.doaxiscreate, axisname):
238            return
239        self.doaxispositioner(axisname)
240        self.axes[axisname].create()
241
242    def dolayout(self):
243        raise NotImplementedError
244
245    def dobackground(self):
246        pass
247
248    def doaxes(self):
249        raise NotImplementedError
250
251    def dostyles(self):
252        if self.did(self.dostyles):
253            return
254        self.dolayout()
255        self.dobackground()
256
257        # count the usage of styles and perform selects
258        styletotal = {}
259        def stylesid(styles):
260            return ":".join([str(id(style)) for style in styles])
261        for plotitem in self.plotitems:
262            try:
263                styletotal[stylesid(plotitem.styles)] += 1
264            except:
265                styletotal[stylesid(plotitem.styles)] = 1
266        styleindex = {}
267        for plotitem in self.plotitems:
268            try:
269                styleindex[stylesid(plotitem.styles)] += 1
270            except:
271                styleindex[stylesid(plotitem.styles)] = 0
272            plotitem.selectstyles(self, styleindex[stylesid(plotitem.styles)],
273                                        styletotal[stylesid(plotitem.styles)])
274
275        self.didstyles = 1
276
277    def doplotitem(self, plotitem):
278        if self.did(self.doplotitem, plotitem):
279            return
280        self.dostyles()
281        plotitem.draw(self)
282
283    def doplot(self):
284        for plotitem in self.plotitems:
285            self.doplotitem(plotitem)
286
287    def dodata(self):
288        logger.warning("dodata() has been deprecated. Use doplot() instead.")
289        self.doplot()
290
291    def dokeyitem(self, plotitem):
292        if self.did(self.dokeyitem, plotitem):
293            return
294        self.dostyles()
295        if plotitem.title is not None:
296            self.keyitems.append(plotitem)
297
298    def dokey(self):
299        raise NotImplementedError
300
301    def finish(self):
302        self.dobackground()
303        self.doaxes()
304        self.doplot()
305        self.dokey()
306
307
308class graphxy(graph):
309
310    def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
311                 key=None, backgroundattrs=None, axesdist=0.8*unit.v_cm, flipped=False,
312                 xaxisat=None, yaxisat=None, **axes):
313        graph.__init__(self)
314
315        self.xpos = xpos
316        self.ypos = ypos
317        self.xpos_pt = unit.topt(self.xpos)
318        self.ypos_pt = unit.topt(self.ypos)
319        self.xaxisat = xaxisat
320        self.yaxisat = yaxisat
321        self.key = key
322        self.backgroundattrs = backgroundattrs
323        self.axesdist_pt = unit.topt(axesdist)
324        self.flipped = flipped
325
326        self.width = width
327        self.height = height
328        if width is None:
329            if height is None:
330                raise ValueError("specify width and/or height")
331            else:
332                self.width = ratio * self.height
333        elif height is None:
334            self.height = (1.0/ratio) * self.width
335        self.width_pt = unit.topt(self.width)
336        self.height_pt = unit.topt(self.height)
337
338        for axisname, aaxis in list(axes.items()):
339            if aaxis is not None:
340                if not isinstance(aaxis, axis.linkedaxis):
341                    self.axes[axisname] = axis.anchoredaxis(aaxis, self.textengine, axisname)
342                else:
343                    self.axes[axisname] = aaxis
344        for axisname, axisat in [("x", xaxisat), ("y", yaxisat)]:
345            okey = axisname + "2"
346            if axisname not in axes:
347                if okey not in axes or axes[okey] is None:
348                    self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.textengine, axisname)
349                    if okey not in axes:
350                        self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
351                else:
352                    self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
353            elif okey not in axes and axisat is None:
354                self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
355
356        if "x" in self.axes:
357            self.xbasepath = self.axes["x"].basepath
358            self.xvbasepath = self.axes["x"].vbasepath
359            self.xgridpath = self.axes["x"].gridpath
360            self.xtickpoint_pt = self.axes["x"].tickpoint_pt
361            self.xtickpoint = self.axes["x"].tickpoint
362            self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
363            self.xvtickpoint = self.axes["x"].tickpoint
364            self.xtickdirection = self.axes["x"].tickdirection
365            self.xvtickdirection = self.axes["x"].vtickdirection
366
367        if "y" in self.axes:
368            self.ybasepath = self.axes["y"].basepath
369            self.yvbasepath = self.axes["y"].vbasepath
370            self.ygridpath = self.axes["y"].gridpath
371            self.ytickpoint_pt = self.axes["y"].tickpoint_pt
372            self.ytickpoint = self.axes["y"].tickpoint
373            self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
374            self.yvtickpoint = self.axes["y"].vtickpoint
375            self.ytickdirection = self.axes["y"].tickdirection
376            self.yvtickdirection = self.axes["y"].vtickdirection
377
378        self.axesnames = ([], [])
379        for axisname, aaxis in list(self.axes.items()):
380            if axisname[0] not in "xy" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
381                                                                   axisname[1:] == "1")):
382                raise ValueError("invalid axis name")
383            if axisname[0] == "x":
384                self.axesnames[0].append(axisname)
385            else:
386                self.axesnames[1].append(axisname)
387            aaxis.setcreatecall(self.doaxiscreate, axisname)
388
389        self.axespositioners = dict(x=positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
390                                                                self.xpos_pt + self.width_pt, self.ypos_pt,
391                                                                (0, 1), self.xvgridpath),
392                                    x2=positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt + self.height_pt,
393                                                                 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
394                                                                 (0, -1), self.xvgridpath),
395                                    y=positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
396                                                                self.xpos_pt, self.ypos_pt + self.height_pt,
397                                                                (1, 0), self.yvgridpath),
398                                    y2=positioner.lineaxispos_pt(self.xpos_pt + self.width_pt, self.ypos_pt,
399                                                                 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
400                                                                 (-1, 0), self.yvgridpath))
401        if self.flipped:
402            self.axespositioners = dict(x=self.axespositioners["y2"],
403                                        y2=self.axespositioners["x2"],
404                                        y=self.axespositioners["x"],
405                                        x2=self.axespositioners["y"])
406
407    def pos_pt(self, x, y, xaxis=None, yaxis=None):
408        if xaxis is None:
409            xaxis = self.axes["x"]
410        if yaxis is None:
411            yaxis = self.axes["y"]
412        vx = xaxis.convert(x)
413        vy = yaxis.convert(y)
414        if self.flipped:
415            vx, vy = vy, vx
416        return (self.xpos_pt + vx*self.width_pt,
417                self.ypos_pt + vy*self.height_pt)
418
419    def pos(self, x, y, xaxis=None, yaxis=None):
420        if xaxis is None:
421            xaxis = self.axes["x"]
422        if yaxis is None:
423            yaxis = self.axes["y"]
424        vx = xaxis.convert(x)
425        vy = yaxis.convert(y)
426        if self.flipped:
427            vx, vy = vy, vx
428        return (self.xpos + vx*self.width,
429                self.ypos + vy*self.height)
430
431    def vpos_pt(self, vx, vy):
432        if self.flipped:
433            vx, vy = vy, vx
434        return (self.xpos_pt + vx*self.width_pt,
435                self.ypos_pt + vy*self.height_pt)
436
437    def vpos(self, vx, vy):
438        if self.flipped:
439            vx, vy = vy, vx
440        return (self.xpos + vx*self.width,
441                self.ypos + vy*self.height)
442
443    def vzindex(self, vx, vy):
444        return 0
445
446    def vangle(self, vx1, vy1, vx2, vy2, vx3, vy3):
447        return 1
448
449    def vgeodesic(self, vx1, vy1, vx2, vy2):
450        """returns a geodesic path between two points in graph coordinates"""
451        if self.flipped:
452            vx1, vy1 = vy1, vx1
453            vx2, vy2 = vy2, vx2
454        return path.line_pt(self.xpos_pt + vx1*self.width_pt,
455                            self.ypos_pt + vy1*self.height_pt,
456                            self.xpos_pt + vx2*self.width_pt,
457                            self.ypos_pt + vy2*self.height_pt)
458
459    def vgeodesic_el(self, vx1, vy1, vx2, vy2):
460        """returns a geodesic path element between two points in graph coordinates"""
461        if self.flipped:
462            vx1, vy1 = vy1, vx1
463            vx2, vy2 = vy2, vx2
464        return path.lineto_pt(self.xpos_pt + vx2*self.width_pt,
465                              self.ypos_pt + vy2*self.height_pt)
466
467    def vcap_pt(self, coordinate, length_pt, vx, vy):
468        """returns an error cap path for a given coordinate, lengths and
469        point in graph coordinates"""
470        if self.flipped:
471            coordinate = 1-coordinate
472            vx, vy = vy, vx
473        if coordinate == 0:
474            return path.line_pt(self.xpos_pt + vx*self.width_pt - 0.5*length_pt,
475                                self.ypos_pt + vy*self.height_pt,
476                                self.xpos_pt + vx*self.width_pt + 0.5*length_pt,
477                                self.ypos_pt + vy*self.height_pt)
478        elif coordinate == 1:
479            return path.line_pt(self.xpos_pt + vx*self.width_pt,
480                                self.ypos_pt + vy*self.height_pt - 0.5*length_pt,
481                                self.xpos_pt + vx*self.width_pt,
482                                self.ypos_pt + vy*self.height_pt + 0.5*length_pt)
483        else:
484            raise ValueError("direction invalid")
485
486    def xvgridpath(self, vx):
487        return path.line_pt(self.xpos_pt + vx*self.width_pt, self.ypos_pt,
488                            self.xpos_pt + vx*self.width_pt, self.ypos_pt + self.height_pt)
489
490    def yvgridpath(self, vy):
491        return path.line_pt(self.xpos_pt, self.ypos_pt + vy*self.height_pt,
492                            self.xpos_pt + self.width_pt, self.ypos_pt + vy*self.height_pt)
493
494    def autokeygraphattrs(self):
495        return dict(direction="vertical", length=self.height)
496
497    def autokeygraphtrafo(self, keygraph):
498        dependsonaxisnumber = None
499        if self.flipped:
500            dependsonaxisname = "x"
501        else:
502            dependsonaxisname = "y"
503        for axisname in self.axes:
504            if axisname[0] == dependsonaxisname:
505                if len(axisname) == 1:
506                    axisname += "1"
507                axisnumber = int(axisname[1:])
508                if not (axisnumber % 2) and not self.flipped or (axisnumber % 2) and self.flipped:
509                    if dependsonaxisnumber is None or dependsonaxisnumber < axisnumber:
510                        dependsonaxisnumber = axisnumber
511        if dependsonaxisnumber is None:
512            x_pt = self.xpos_pt + self.width_pt
513        else:
514            if dependsonaxisnumber > 1:
515                dependsonaxisname += str(dependsonaxisnumber)
516            self.doaxiscreate(dependsonaxisname)
517            x_pt = self.axes[dependsonaxisname].positioner.x1_pt + self.axes[dependsonaxisname].canvas.extent_pt
518        x_pt += self.axesdist_pt
519        return trafo.translate_pt(x_pt, self.ypos_pt)
520
521    def axisatv(self, axis, v):
522        if axis.positioner.fixtickdirection[0]:
523            # it is a y-axis
524            t = trafo.translate_pt(self.xpos_pt + v*self.width_pt - axis.positioner.x1_pt, 0)
525        else:
526            # it is an x-axis
527            t = trafo.translate_pt(0, self.ypos_pt + v*self.height_pt - axis.positioner.y1_pt)
528        c = canvas.canvas()
529        for layer, subcanvas in list(axis.canvas.layers.items()):
530            c.layer(layer).insert(subcanvas, [t])
531        assert len(axis.canvas.layers) == len(axis.canvas.items), str(axis.canvas.items)
532        axis.canvas = c
533
534    def doaxispositioner(self, axisname):
535        if self.did(self.doaxispositioner, axisname):
536            return
537        self.doranges()
538        if axisname in ["x", "x2", "y", "y2"]:
539            self.axes[axisname].setpositioner(self.axespositioners[axisname])
540        else:
541            if axisname[1:] == "3":
542                dependsonaxisname = axisname[0]
543            else:
544                dependsonaxisname = "%s%d" % (axisname[0], int(axisname[1:]) - 2)
545            self.doaxiscreate(dependsonaxisname)
546            sign = 2*(int(axisname[1:]) % 2) - 1
547            if axisname[0] == "x" and self.flipped:
548                sign = -sign
549            if axisname[0] == "x" and not self.flipped or axisname[0] == "y" and self.flipped:
550                y_pt = self.axes[dependsonaxisname].positioner.y1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
551                self.axes[axisname].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, y_pt,
552                                                                            self.xpos_pt + self.width_pt, y_pt,
553                                                                            (0, sign), self.xvgridpath))
554            else:
555                x_pt = self.axes[dependsonaxisname].positioner.x1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
556                self.axes[axisname].setpositioner(positioner.lineaxispos_pt(x_pt, self.ypos_pt,
557                                                                            x_pt, self.ypos_pt + self.height_pt,
558                                                                            (sign, 0), self.yvgridpath))
559
560    def dolayout(self):
561        if self.did(self.dolayout):
562            return
563        for axisname in list(self.axes.keys()):
564            self.doaxiscreate(axisname)
565        if self.xaxisat is not None:
566            self.axisatv(self.axes["x"], self.axes["y"].convert(self.xaxisat))
567        if self.yaxisat is not None:
568            self.axisatv(self.axes["y"], self.axes["x"].convert(self.yaxisat))
569
570    def dobackground(self):
571        if self.did(self.dobackground):
572            return
573        if self.backgroundattrs is not None:
574            self.layer("background").draw(path.rect_pt(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt),
575                                          self.backgroundattrs)
576
577    def doaxes(self):
578        if self.did(self.doaxes):
579            return
580        self.dolayout()
581        self.dobackground()
582        for axis in list(self.axes.values()):
583            for layer, canvas in list(axis.canvas.layers.items()):
584                self.layer("axes.%s" % layer).insert(canvas)
585            assert len(axis.canvas.layers) == len(axis.canvas.items), str(axis.canvas.items)
586
587    def dokey(self):
588        if self.did(self.dokey):
589            return
590        self.dobackground()
591        for plotitem in self.plotitems:
592            self.dokeyitem(plotitem)
593        if self.key is not None:
594            c = self.key.paint(self.keyitems)
595            bbox = c.bbox()
596            def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
597                ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
598                cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
599                return ppos-cpos
600            if bbox:
601                x = parentchildalign(self.xpos_pt, self.xpos_pt+self.width_pt,
602                                     bbox.llx_pt, bbox.urx_pt,
603                                     self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
604                y = parentchildalign(self.ypos_pt, self.ypos_pt+self.height_pt,
605                                     bbox.lly_pt, bbox.ury_pt,
606                                     self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
607                self.layer("key").insert(c, [trafo.translate_pt(x, y)])
608
609
610
611class graphx(graphxy):
612
613    def __init__(self, xpos=0, ypos=0, length=None, size=0.5*unit.v_cm, direction="vertical",
614                 key=None, backgroundattrs=None, axesdist=0.8*unit.v_cm, **axes):
615        for name in axes:
616            if not name.startswith("x"):
617                raise ValueError("Only x axes are allowed")
618        self.direction = direction
619        if self.direction == "vertical":
620            kwargsxy = dict(width=size, height=length, flipped=True)
621        elif self.direction == "horizontal":
622            kwargsxy = dict(width=length, height=size)
623        else:
624            raise ValueError("vertical or horizontal direction required")
625        kwargsxy.update(**axes)
626
627        graphxy.__init__(self, xpos=xpos, ypos=ypos, ratio=None, key=key, y=axis.lin(min=0, max=1, parter=None),
628                         backgroundattrs=backgroundattrs, axesdist=axesdist, **kwargsxy)
629
630    def pos_pt(self, x, xaxis=None):
631        return graphxy.pos_pt(self, x, 0.5, xaxis)
632
633    def pos(self, x, xaxis=None):
634        return graphxy.pos(self, x, 0.5, xaxis)
635
636    def vpos_pt(self, vx):
637        return graphxy.vpos_pt(self, vx, 0.5)
638
639    def vpos(self, vx):
640        return graphxy.vpos(self, vx, 0.5)
641
642    def vgeodesic(self, vx1, vx2):
643        return graphxy.vgeodesic(self, vx1, 0.5, vx2, 0.5)
644
645    def vgeodesic_el(self, vx1, vy1, vx2, vy2):
646        return graphxy.vgeodesic_el(self, vx1, 0.5, vx2, 0.5)
647
648    def vcap_pt(self, coordinate, length_pt, vx):
649        if coordinate == 0:
650            return graphxy.vcap_pt(self, coordinate, length_pt, vx, 0.5)
651        else:
652            raise ValueError("direction invalid")
653
654    def xvgridpath(self, vx):
655        return graphxy.xvgridpath(self, vx)
656
657    def yvgridpath(self, vy):
658        raise Exception("This method does not exist on a one dimensional graph.")
659
660    def axisatv(self, axis, v):
661        raise Exception("This method does not exist on a one dimensional graph.")
662
663
664
665class graphxyz(graph):
666
667    class central:
668
669        def __init__(self, distance, phi, theta, anglefactor=math.pi/180):
670            phi *= anglefactor
671            theta *= anglefactor
672            self.distance = distance
673
674            self.a = (-math.sin(phi), math.cos(phi), 0)
675            self.b = (-math.cos(phi)*math.sin(theta),
676                      -math.sin(phi)*math.sin(theta),
677                      math.cos(theta))
678            self.eye = (distance*math.cos(phi)*math.cos(theta),
679                        distance*math.sin(phi)*math.cos(theta),
680                        distance*math.sin(theta))
681
682        def point(self, x, y, z):
683            d0 = (self.a[0]*self.b[1]*(z-self.eye[2])
684                + self.a[2]*self.b[0]*(y-self.eye[1])
685                + self.a[1]*self.b[2]*(x-self.eye[0])
686                - self.a[2]*self.b[1]*(x-self.eye[0])
687                - self.a[0]*self.b[2]*(y-self.eye[1])
688                - self.a[1]*self.b[0]*(z-self.eye[2]))
689            da = (self.eye[0]*self.b[1]*(z-self.eye[2])
690                + self.eye[2]*self.b[0]*(y-self.eye[1])
691                + self.eye[1]*self.b[2]*(x-self.eye[0])
692                - self.eye[2]*self.b[1]*(x-self.eye[0])
693                - self.eye[0]*self.b[2]*(y-self.eye[1])
694                - self.eye[1]*self.b[0]*(z-self.eye[2]))
695            db = (self.a[0]*self.eye[1]*(z-self.eye[2])
696                + self.a[2]*self.eye[0]*(y-self.eye[1])
697                + self.a[1]*self.eye[2]*(x-self.eye[0])
698                - self.a[2]*self.eye[1]*(x-self.eye[0])
699                - self.a[0]*self.eye[2]*(y-self.eye[1])
700                - self.a[1]*self.eye[0]*(z-self.eye[2]))
701            return da/d0, db/d0
702
703        def zindex(self, x, y, z):
704            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
705
706        def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
707            sx = (x1-self.eye[0])
708            sy = (y1-self.eye[1])
709            sz = (z1-self.eye[2])
710            nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
711            ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
712            nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
713            return (sx*nx+sy*ny+sz*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)/math.sqrt(sx*sx+sy*sy+sz*sz)
714
715
716    class parallel:
717
718        def __init__(self, phi, theta, anglefactor=math.pi/180):
719            phi *= anglefactor
720            theta *= anglefactor
721
722            self.a = (-math.sin(phi), math.cos(phi), 0)
723            self.b = (-math.cos(phi)*math.sin(theta),
724                      -math.sin(phi)*math.sin(theta),
725                      math.cos(theta))
726            self.c = (-math.cos(phi)*math.cos(theta),
727                      -math.sin(phi)*math.cos(theta),
728                      -math.sin(theta))
729
730        def point(self, x, y, z):
731            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
732
733        def zindex(self, x, y, z):
734            return self.c[0]*x+self.c[1]*y+self.c[2]*z
735
736        def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
737            nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
738            ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
739            nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
740            return (self.c[0]*nx+self.c[1]*ny+self.c[2]*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)
741
742
743    def __init__(self, xpos=0, ypos=0, size=None,
744                 xscale=1, yscale=1, zscale=1/goldenmean, xy12axesat=None, xy12axesatname="z",
745                 projector=central(10, -30, 30), axesdist=0.8*unit.v_cm, key=None,
746                 **axes):
747        graph.__init__(self)
748        for name in ["hiddenaxes.grid", "hiddenaxes.baseline", "hiddenaxes.ticks", "hiddenaxes.labels", "hiddenaxes.title"]:
749            self.layer(name)
750        self.layer("hiddenaxes", below="filldata")
751
752        self.xpos = xpos
753        self.ypos = ypos
754        self.size = size
755        self.xpos_pt = unit.topt(xpos)
756        self.ypos_pt = unit.topt(ypos)
757        self.size_pt = unit.topt(size)
758        self.xscale = xscale
759        self.yscale = yscale
760        self.zscale = zscale
761        self.xy12axesat = xy12axesat
762        self.xy12axesatname = xy12axesatname
763        self.projector = projector
764        self.axesdist_pt = unit.topt(axesdist)
765        self.key = key
766
767        self.xorder = projector.zindex(0, -1, 0) > projector.zindex(0, 1, 0) and 1 or 0
768        self.yorder = projector.zindex(-1, 0, 0) > projector.zindex(1, 0, 0) and 1 or 0
769        self.zindexscale = math.sqrt(xscale*xscale+yscale*yscale+zscale*zscale)
770
771        # the pXYshow attributes are booleans stating whether plane perpendicular to axis X
772        # at the virtual graph coordinate Y will be hidden by data or not. An axis is considered
773        # to be visible if one of the two planes it is part of is visible. Other axes are drawn
774        # in the hiddenaxes layer (i.e. layer group).
775        # TODO: Tick and grid visibility is treated like the axis visibility at the moment.
776        self.pz0show = self.vangle(0, 0, 0, 1, 0, 0, 1, 1, 0) > 0
777        self.pz1show = self.vangle(0, 0, 1, 0, 1, 1, 1, 1, 1) > 0
778        self.py0show = self.vangle(0, 0, 0, 0, 0, 1, 1, 0, 1) > 0
779        self.py1show = self.vangle(0, 1, 0, 1, 1, 0, 1, 1, 1) > 0
780        self.px0show = self.vangle(0, 0, 0, 0, 1, 0, 0, 1, 1) > 0
781        self.px1show = self.vangle(1, 0, 0, 1, 0, 1, 1, 1, 1) > 0
782
783        for axisname, aaxis in list(axes.items()):
784            if aaxis is not None:
785                if not isinstance(aaxis, axis.linkedaxis):
786                    self.axes[axisname] = axis.anchoredaxis(aaxis, self.textengine, axisname)
787                else:
788                    self.axes[axisname] = aaxis
789        for axisname in ["x", "y"]:
790            okey = axisname + "2"
791            if axisname not in axes:
792                if okey not in axes or axes[okey] is None:
793                    self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.textengine, axisname)
794                    if okey not in axes:
795                        self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
796                else:
797                    self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
798            elif okey not in axes:
799                self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
800        if "z" not in axes:
801            self.axes["z"] = axis.anchoredaxis(axis.linear(), self.textengine, "z")
802
803        if "x" in self.axes:
804            self.xbasepath = self.axes["x"].basepath
805            self.xvbasepath = self.axes["x"].vbasepath
806            self.xgridpath = self.axes["x"].gridpath
807            self.xtickpoint_pt = self.axes["x"].tickpoint_pt
808            self.xtickpoint = self.axes["x"].tickpoint
809            self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
810            self.xvtickpoint = self.axes["x"].tickpoint
811            self.xtickdirection = self.axes["x"].tickdirection
812            self.xvtickdirection = self.axes["x"].vtickdirection
813
814        if "y" in self.axes:
815            self.ybasepath = self.axes["y"].basepath
816            self.yvbasepath = self.axes["y"].vbasepath
817            self.ygridpath = self.axes["y"].gridpath
818            self.ytickpoint_pt = self.axes["y"].tickpoint_pt
819            self.ytickpoint = self.axes["y"].tickpoint
820            self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
821            self.yvtickpoint = self.axes["y"].vtickpoint
822            self.ytickdirection = self.axes["y"].tickdirection
823            self.yvtickdirection = self.axes["y"].vtickdirection
824
825        if "z" in self.axes:
826            self.zbasepath = self.axes["z"].basepath
827            self.zvbasepath = self.axes["z"].vbasepath
828            self.zgridpath = self.axes["z"].gridpath
829            self.ztickpoint_pt = self.axes["z"].tickpoint_pt
830            self.ztickpoint = self.axes["z"].tickpoint
831            self.zvtickpoint_pt = self.axes["z"].vtickpoint
832            self.zvtickpoint = self.axes["z"].vtickpoint
833            self.ztickdirection = self.axes["z"].tickdirection
834            self.zvtickdirection = self.axes["z"].vtickdirection
835
836        self.axesnames = ([], [], [])
837        for axisname, aaxis in list(self.axes.items()):
838            if axisname[0] not in "xyz" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
839                                                                    axisname[1:] == "1")):
840                raise ValueError("invalid axis name")
841            if axisname[0] == "x":
842                self.axesnames[0].append(axisname)
843            elif axisname[0] == "y":
844                self.axesnames[1].append(axisname)
845            else:
846                self.axesnames[2].append(axisname)
847            aaxis.setcreatecall(self.doaxiscreate, axisname)
848
849    def pos_pt(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
850        if xaxis is None:
851            xaxis = self.axes["x"]
852        if yaxis is None:
853            yaxis = self.axes["y"]
854        if zaxis is None:
855            zaxis = self.axes["z"]
856        return self.vpos_pt(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
857
858    def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
859        if xaxis is None:
860            xaxis = self.axes["x"]
861        if yaxis is None:
862            yaxis = self.axes["y"]
863        if zaxis is None:
864            zaxis = self.axes["z"]
865        return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
866
867    def vpos_pt(self, vx, vy, vz):
868        x, y = self.projector.point(2*self.xscale*(vx - 0.5),
869                                    2*self.yscale*(vy - 0.5),
870                                    2*self.zscale*(vz - 0.5))
871        return self.xpos_pt+x*self.size_pt, self.ypos_pt+y*self.size_pt
872
873    def vpos(self, vx, vy, vz):
874        x, y = self.projector.point(2*self.xscale*(vx - 0.5),
875                                    2*self.yscale*(vy - 0.5),
876                                    2*self.zscale*(vz - 0.5))
877        return self.xpos+x*self.size, self.ypos+y*self.size
878
879    def vzindex(self, vx, vy, vz):
880        return self.projector.zindex(2*self.xscale*(vx - 0.5),
881                                     2*self.yscale*(vy - 0.5),
882                                     2*self.zscale*(vz - 0.5))/self.zindexscale
883
884    def vangle(self, vx1, vy1, vz1, vx2, vy2, vz2, vx3, vy3, vz3):
885        return self.projector.angle(2*self.xscale*(vx1 - 0.5),
886                                    2*self.yscale*(vy1 - 0.5),
887                                    2*self.zscale*(vz1 - 0.5),
888                                    2*self.xscale*(vx2 - 0.5),
889                                    2*self.yscale*(vy2 - 0.5),
890                                    2*self.zscale*(vz2 - 0.5),
891                                    2*self.xscale*(vx3 - 0.5),
892                                    2*self.yscale*(vy3 - 0.5),
893                                    2*self.zscale*(vz3 - 0.5))
894
895    def vgeodesic(self, vx1, vy1, vz1, vx2, vy2, vz2):
896        """returns a geodesic path between two points in graph coordinates"""
897        return path.line_pt(*(self.vpos_pt(vx1, vy1, vz1) + self.vpos_pt(vx2, vy2, vz2)))
898
899    def vgeodesic_el(self, vx1, vy1, vz1, vx2, vy2, vz2):
900        """returns a geodesic path element between two points in graph coordinates"""
901        return path.lineto_pt(*self.vpos_pt(vx2, vy2, vz2))
902
903    def vcap_pt(self, coordinate, length_pt, vx, vy, vz):
904        """returns an error cap path for a given coordinate, lengths and
905        point in graph coordinates"""
906        if coordinate == 0:
907            return self.vgeodesic(vx-0.5*length_pt/self.size_pt, vy, vz, vx+0.5*length_pt/self.size_pt, vy, vz)
908        elif coordinate == 1:
909            return self.vgeodesic(vx, vy-0.5*length_pt/self.size_pt, vz, vx, vy+0.5*length_pt/self.size_pt, vz)
910        elif coordinate == 2:
911            return self.vgeodesic(vx, vy, vz-0.5*length_pt/self.size_pt, vx, vy, vz+0.5*length_pt/self.size_pt)
912        else:
913            raise ValueError("direction invalid")
914
915    def xvtickdirection(self, vx):
916        if self.xorder:
917            x1_pt, y1_pt = self.vpos_pt(vx, 1, 0)
918            x2_pt, y2_pt = self.vpos_pt(vx, 0, 0)
919        else:
920            x1_pt, y1_pt = self.vpos_pt(vx, 0, 0)
921            x2_pt, y2_pt = self.vpos_pt(vx, 1, 0)
922        dx_pt = x2_pt - x1_pt
923        dy_pt = y2_pt - y1_pt
924        norm = math.hypot(dx_pt, dy_pt)
925        return dx_pt/norm, dy_pt/norm
926
927    def yvtickdirection(self, vy):
928        if self.yorder:
929            x1_pt, y1_pt = self.vpos_pt(1, vy, 0)
930            x2_pt, y2_pt = self.vpos_pt(0, vy, 0)
931        else:
932            x1_pt, y1_pt = self.vpos_pt(0, vy, 0)
933            x2_pt, y2_pt = self.vpos_pt(1, vy, 0)
934        dx_pt = x2_pt - x1_pt
935        dy_pt = y2_pt - y1_pt
936        norm = math.hypot(dx_pt, dy_pt)
937        return dx_pt/norm, dy_pt/norm
938
939    def vtickdirection(self, vx1, vy1, vz1, vx2, vy2, vz2):
940        x1_pt, y1_pt = self.vpos_pt(vx1, vy1, vz1)
941        x2_pt, y2_pt = self.vpos_pt(vx2, vy2, vz2)
942        dx_pt = x2_pt - x1_pt
943        dy_pt = y2_pt - y1_pt
944        norm = math.hypot(dx_pt, dy_pt)
945        return dx_pt/norm, dy_pt/norm
946
947    def xvgridpath(self, vx):
948        return path.path(path.moveto_pt(*self.vpos_pt(vx, 0, 0)),
949                         path.lineto_pt(*self.vpos_pt(vx, 1, 0)),
950                         path.lineto_pt(*self.vpos_pt(vx, 1, 1)),
951                         path.lineto_pt(*self.vpos_pt(vx, 0, 1)),
952                         path.closepath())
953
954    def yvgridpath(self, vy):
955        return path.path(path.moveto_pt(*self.vpos_pt(0, vy, 0)),
956                         path.lineto_pt(*self.vpos_pt(1, vy, 0)),
957                         path.lineto_pt(*self.vpos_pt(1, vy, 1)),
958                         path.lineto_pt(*self.vpos_pt(0, vy, 1)),
959                         path.closepath())
960
961    def zvgridpath(self, vz):
962        return path.path(path.moveto_pt(*self.vpos_pt(0, 0, vz)),
963                         path.lineto_pt(*self.vpos_pt(1, 0, vz)),
964                         path.lineto_pt(*self.vpos_pt(1, 1, vz)),
965                         path.lineto_pt(*self.vpos_pt(0, 1, vz)),
966                         path.closepath())
967
968    def autokeygraphattrs(self):
969        return dict(direction="vertical", length=self.size)
970
971    def autokeygraphtrafo(self, keygraph):
972        self.doaxes()
973        x_pt = self.layer("axes").bbox().right_pt() + self.axesdist_pt
974        y_pt = 0.5*(self.layer("axes").bbox().top_pt() + self.layer("axes").bbox().bottom_pt() - self.size_pt)
975        return trafo.translate_pt(x_pt, y_pt)
976
977    def doaxispositioner(self, axisname):
978        if self.did(self.doaxispositioner, axisname):
979            return
980        self.doranges()
981        if self.xy12axesat is not None:
982            self.doaxiscreate(self.xy12axesatname)
983            self.doaxispositioner(self.xy12axesatname)
984            xy12axesatv = self.axes[self.xy12axesatname].convert(self.xy12axesat)
985        else:
986            xy12axesatv = 0
987        if axisname == "x":
988            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, xy12axesatv),
989                                                                            lambda vx: self.vtickdirection(vx, self.xorder, 0, vx, 1-self.xorder, xy12axesatv),
990                                                                            self.xvgridpath))
991            if self.xorder:
992                self.axes[axisname].hidden = not self.py1show and not self.pz0show
993            else:
994                self.axes[axisname].hidden = not self.py0show and not self.pz0show
995        elif axisname == "x2":
996            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, xy12axesatv),
997                                                                            lambda vx: self.vtickdirection(vx, 1-self.xorder, 0, vx, self.xorder, xy12axesatv),
998                                                                            self.xvgridpath))
999            if self.xorder:
1000                self.axes[axisname].hidden = not self.py0show and not self.pz0show
1001            else:
1002                self.axes[axisname].hidden = not self.py1show and not self.pz0show
1003        elif axisname == "x3":
1004            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 1),
1005                                                                            lambda vx: self.vtickdirection(vx, self.xorder, 1, vx, 1-self.xorder, 1),
1006                                                                            self.xvgridpath))
1007            if self.xorder:
1008                self.axes[axisname].hidden = not self.py1show and not self.pz1show
1009            else:
1010                self.axes[axisname].hidden = not self.py0show and not self.pz1show
1011        elif axisname == "x4":
1012            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 1),
1013                                                                            lambda vx: self.vtickdirection(vx, 1-self.xorder, 1, vx, self.xorder, 1),
1014                                                                            self.xvgridpath))
1015            if self.xorder:
1016                self.axes[axisname].hidden = not self.py0show and not self.pz1show
1017            else:
1018                self.axes[axisname].hidden = not self.py1show and not self.pz1show
1019        elif axisname == "y":
1020            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, xy12axesatv),
1021                                                                            lambda vy: self.vtickdirection(self.yorder, vy, 0, 1-self.yorder, vy, xy12axesatv),
1022                                                                            self.yvgridpath))
1023            if self.yorder:
1024                self.axes[axisname].hidden = not self.px1show and not self.pz0show
1025            else:
1026                self.axes[axisname].hidden = not self.px0show and not self.pz0show
1027        elif axisname == "y2":
1028            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, xy12axesatv),
1029                                                                            lambda vy: self.vtickdirection(1-self.yorder, vy, 0, self.yorder, vy, xy12axesatv),
1030                                                                            self.yvgridpath))
1031            if self.yorder:
1032                self.axes[axisname].hidden = not self.px0show and not self.pz0show
1033            else:
1034                self.axes[axisname].hidden = not self.px1show and not self.pz0show
1035        elif axisname == "y3":
1036            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 1),
1037                                                                            lambda vy: self.vtickdirection(self.yorder, vy, 1, 1-self.yorder, vy, 1),
1038                                                                            self.yvgridpath))
1039            if self.yorder:
1040                self.axes[axisname].hidden = not self.px1show and not self.pz1show
1041            else:
1042                self.axes[axisname].hidden = not self.px0show and not self.pz1show
1043        elif axisname == "y4":
1044            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 1),
1045                                                                            lambda vy: self.vtickdirection(1-self.yorder, vy, 1, self.yorder, vy, 1),
1046                                                                            self.yvgridpath))
1047            if self.yorder:
1048                self.axes[axisname].hidden = not self.px0show and not self.pz1show
1049            else:
1050                self.axes[axisname].hidden = not self.px1show and not self.pz1show
1051        elif axisname == "z":
1052            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 0, vz),
1053                                                                            lambda vz: self.vtickdirection(0, 0, vz, 1, 1, vz),
1054                                                                            self.zvgridpath))
1055            self.axes[axisname].hidden = not self.px0show and not self.py0show
1056        elif axisname == "z2":
1057            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 0, vz),
1058                                                                            lambda vz: self.vtickdirection(1, 0, vz, 0, 1, vz),
1059                                                                            self.zvgridpath))
1060            self.axes[axisname].hidden = not self.px1show and not self.py0show
1061        elif axisname == "z3":
1062            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 1, vz),
1063                                                                            lambda vz: self.vtickdirection(0, 1, vz, 1, 0, vz),
1064                                                                            self.zvgridpath))
1065            self.axes[axisname].hidden = not self.px0show and not self.py1show
1066        elif axisname == "z4":
1067            self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 1, vz),
1068                                                                            lambda vz: self.vtickdirection(1, 1, vz, 0, 0, vz),
1069                                                                            self.zvgridpath))
1070            self.axes[axisname].hidden = not self.px1show and not self.py1show
1071        else:
1072            raise NotImplementedError("4 axis per dimension supported only")
1073
1074    def dolayout(self):
1075        if self.did(self.dolayout):
1076            return
1077        for axisname in list(self.axes.keys()):
1078            self.doaxiscreate(axisname)
1079
1080    def dobackground(self):
1081        if self.did(self.dobackground):
1082            return
1083
1084    def doaxes(self):
1085        if self.did(self.doaxes):
1086            return
1087        self.dolayout()
1088        self.dobackground()
1089        for axis in list(self.axes.values()):
1090            if axis.hidden:
1091                self.layer("hiddenaxes").insert(axis.canvas)
1092            else:
1093                self.layer("axes").insert(axis.canvas)
1094
1095    def dokey(self):
1096        if self.did(self.dokey):
1097            return
1098        self.dobackground()
1099        for plotitem in self.plotitems:
1100            self.dokeyitem(plotitem)
1101        if self.key is not None:
1102            c = self.key.paint(self.keyitems)
1103            bbox = c.bbox()
1104            def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
1105                ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
1106                cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
1107                return ppos-cpos
1108            if bbox:
1109                x = parentchildalign(self.xpos_pt, self.xpos_pt+self.size_pt,
1110                                     bbox.llx_pt, bbox.urx_pt,
1111                                     self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
1112                y = parentchildalign(self.ypos_pt, self.ypos_pt+self.size_pt,
1113                                     bbox.lly_pt, bbox.ury_pt,
1114                                     self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
1115                self.insert(c, [trafo.translate_pt(x, y)])
1116