prevent double call of _cleanup, which harms usefiles (and is a bad idea in general)
[PyX.git] / pyx / graph / graph.py
bloba7de1c6ce36c9b87e736c5f468ef79ad953287ee
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2012 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2012 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
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.
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.
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
25 import logging, math, re, string
26 from pyx import canvas, path, trafo, unit
27 from pyx.graph.axis import axis, positioner
29 logger = logging.getLogger("pyx")
30 goldenmean = 0.5 * (math.sqrt(5) + 1)
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.
37 _defaultprovider = {}
39 def 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
47 def getdefaultprovider(key):
48 """returns a style, which acts as a default creator for the
49 sharedata variable 'key'"""
50 return _defaultprovider[key]
53 class styledata:
54 """style data storage class
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
65 class plotitem:
67 def __init__(self, graph, data, styles):
68 self.data = data
69 self.title = data.title
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
85 self.styles = styles
86 self.sharedata = styledata()
87 self.dataaxisnames = {}
88 self.privatedatalist = [styledata() for s in self.styles]
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)))
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)
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)
104 def makedynamicdata(self, graph):
105 self.dynamiccolumns = self.data.dynamiccolumns(graph, self.dataaxisnames)
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)
112 def draw(self, graph):
113 for privatedata, style in zip(self.privatedatalist, self.styles):
114 style.initdrawpoints(privatedata, self.sharedata, graph)
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)
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)
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)
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)
158 class graph(canvas.canvas):
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
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
180 def bbox(self):
181 self.finish()
182 return canvas.canvas.bbox(self)
185 def processPS(self, file, writer, context, registry, bbox):
186 self.finish()
187 canvas.canvas.processPS(self, file, writer, context, registry, bbox)
189 def processPDF(self, file, writer, context, registry, bbox):
190 self.finish()
191 canvas.canvas.processPDF(self, file, writer, context, registry, bbox)
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
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
236 def doaxiscreate(self, axisname):
237 if self.did(self.doaxiscreate, axisname):
238 return
239 self.doaxispositioner(axisname)
240 self.axes[axisname].create()
242 def dolayout(self):
243 raise NotImplementedError
245 def dobackground(self):
246 pass
248 def doaxes(self):
249 raise NotImplementedError
251 def dostyles(self):
252 if self.did(self.dostyles):
253 return
254 self.dolayout()
255 self.dobackground()
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)])
275 self.didstyles = 1
277 def doplotitem(self, plotitem):
278 if self.did(self.doplotitem, plotitem):
279 return
280 self.dostyles()
281 plotitem.draw(self)
283 def doplot(self):
284 for plotitem in self.plotitems:
285 self.doplotitem(plotitem)
287 def dodata(self):
288 logger.warning("dodata() has been deprecated. Use doplot() instead.")
289 self.doplot()
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)
298 def dokey(self):
299 raise NotImplementedError
301 def finish(self):
302 self.dobackground()
303 self.doaxes()
304 self.doplot()
305 self.dokey()
308 class graphxy(graph):
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)
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
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)
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.texrunner, 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.texrunner, 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)
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
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
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)
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"])
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)
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)
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)
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)
443 def vzindex(self, vx, vy):
444 return 0
446 def vangle(self, vx1, vy1, vx2, vy2, vx3, vy3):
447 return 1
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)
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)
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")
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)
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)
494 def autokeygraphattrs(self):
495 return dict(direction="vertical", length=self.height)
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)
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
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))
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))
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)
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)
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)])
611 class graphx(graphxy):
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)
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)
630 def pos_pt(self, x, xaxis=None):
631 return graphxy.pos_pt(self, x, 0.5, xaxis)
633 def pos(self, x, xaxis=None):
634 return graphxy.pos(self, x, 0.5, xaxis)
636 def vpos_pt(self, vx):
637 return graphxy.vpos_pt(self, vx, 0.5)
639 def vpos(self, vx):
640 return graphxy.vpos(self, vx, 0.5)
642 def vgeodesic(self, vx1, vx2):
643 return graphxy.vgeodesic(self, vx1, 0.5, vx2, 0.5)
645 def vgeodesic_el(self, vx1, vy1, vx2, vy2):
646 return graphxy.vgeodesic_el(self, vx1, 0.5, vx2, 0.5)
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")
654 def xvgridpath(self, vx):
655 return graphxy.xvgridpath(self, vx)
657 def yvgridpath(self, vy):
658 raise Exception("This method does not exist on a one dimensional graph.")
660 def axisatv(self, axis, v):
661 raise Exception("This method does not exist on a one dimensional graph.")
665 class graphxyz(graph):
667 class central:
669 def __init__(self, distance, phi, theta, anglefactor=math.pi/180):
670 phi *= anglefactor
671 theta *= anglefactor
672 self.distance = distance
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))
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
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
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)
716 class parallel:
718 def __init__(self, phi, theta, anglefactor=math.pi/180):
719 phi *= anglefactor
720 theta *= anglefactor
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))
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
733 def zindex(self, x, y, z):
734 return self.c[0]*x+self.c[1]*y+self.c[2]*z
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)
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")
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
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)
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
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.texrunner, 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.texrunner, 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.texrunner, "z")
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
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
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
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)
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))
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))
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
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
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
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))
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)))
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))
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")
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
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
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
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())
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())
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())
968 def autokeygraphattrs(self):
969 return dict(direction="vertical", length=self.size)
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)
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")
1074 def dolayout(self):
1075 if self.did(self.dolayout):
1076 return
1077 for axisname in list(self.axes.keys()):
1078 self.doaxiscreate(axisname)
1080 def dobackground(self):
1081 if self.did(self.dobackground):
1082 return
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)
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)])