remove spurious print statement
[PyX/mjg.git] / pyx / graph / graph.py
blob8b4a319e3f06d46537052a297a914c2b945aff78
1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2006 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 from __future__ import nested_scopes
27 import math, re, string, warnings
28 from pyx import canvas, path, trafo, unit
29 from pyx.graph import style
30 from pyx.graph.axis import axis, positioner
33 goldenmean = 0.5 * (math.sqrt(5) + 1)
36 class styledata:
37 """style data storage class
39 Instances of this class are used to store data from the styles
40 and to pass point data to the styles by instances named privatedata
41 and sharedata. sharedata is shared between all the style(s) in use
42 by a data instance, while privatedata is private to each style and
43 used as a storage place instead of self to prevent side effects when
44 using a style several times."""
45 pass
48 class plotitem:
50 def __init__(self, graph, data, styles):
51 self.data = data
52 self.title = data.title
54 addstyles = [None]
55 while addstyles:
56 # add styles to ensure all needs of the given styles
57 provided = [] # already provided sharedata variables
58 addstyles = [] # a list of style instances to be added in front
59 for s in styles:
60 for n in s.needsdata:
61 if n not in provided:
62 defaultprovider = style.getdefaultprovider(n)
63 addstyles.append(defaultprovider)
64 provided.extend(defaultprovider.providesdata)
65 provided.extend(s.providesdata)
66 styles = addstyles + styles
68 self.styles = styles
69 self.sharedata = styledata()
70 self.privatedatalist = [styledata() for s in self.styles]
72 # perform setcolumns to all styles
73 self.usedcolumnnames = []
74 for privatedata, s in zip(self.privatedatalist, self.styles):
75 self.usedcolumnnames.extend(s.columnnames(privatedata, self.sharedata, graph, self.data.columnnames))
77 def selectstyles(self, graph, selectindex, selecttotal):
78 for privatedata, style in zip(self.privatedatalist, self.styles):
79 style.selectstyle(privatedata, self.sharedata, graph, selectindex, selecttotal)
81 def adjustaxesstatic(self, graph):
82 for columnname, data in self.data.columns.items():
83 for privatedata, style in zip(self.privatedatalist, self.styles):
84 style.adjustaxis(privatedata, self.sharedata, graph, columnname, data)
86 def makedynamicdata(self, graph):
87 self.dynamiccolumns = self.data.dynamiccolumns(graph)
89 def adjustaxesdynamic(self, graph):
90 for columnname, data in self.dynamiccolumns.items():
91 for privatedata, style in zip(self.privatedatalist, self.styles):
92 style.adjustaxis(privatedata, self.sharedata, graph, columnname, data)
94 def draw(self, graph):
95 for privatedata, style in zip(self.privatedatalist, self.styles):
96 style.initdrawpoints(privatedata, self.sharedata, graph)
97 point = {}
98 useitems = []
99 for columnname in self.usedcolumnnames:
100 try:
101 useitems.append((columnname, self.dynamiccolumns[columnname]))
102 except KeyError:
103 useitems.append((columnname, self.data.columns[columnname]))
104 if not useitems:
105 raise ValueError("cannot draw empty data")
106 for i in xrange(len(useitems[0][1])):
107 for columnname, data in useitems:
108 point[columnname] = data[i]
109 for privatedata, style in zip(self.privatedatalist, self.styles):
110 style.drawpoint(privatedata, self.sharedata, graph, point)
111 for privatedata, style in zip(self.privatedatalist, self.styles):
112 style.donedrawpoints(privatedata, self.sharedata, graph)
114 def key_pt(self, graph, x_pt, y_pt, width_pt, height_pt):
115 for privatedata, style in zip(self.privatedatalist, self.styles):
116 style.key_pt(privatedata, self.sharedata, graph, x_pt, y_pt, width_pt, height_pt)
118 def __getattr__(self, attr):
119 # read only access to the styles privatedata
120 stylesdata = [getattr(styledata, attr)
121 for styledata in self.privatedatalist
122 if hasattr(styledata, attr)]
123 if len(stylesdata) > 1:
124 return stylesdata
125 elif len(stylesdata) == 1:
126 return stylesdata[0]
127 raise AttributeError("access to styledata attribute '%s' failed" % attr)
130 class graph(canvas.canvas):
132 def __init__(self):
133 canvas.canvas.__init__(self)
134 self.axes = {}
135 self.plotitems = []
136 self._calls = {}
137 self.didranges = 0
138 self.didstyles = 0
140 def did(self, method, *args, **kwargs):
141 if not self._calls.has_key(method):
142 self._calls[method] = []
143 for callargs in self._calls[method]:
144 if callargs == (args, kwargs):
145 return 1
146 self._calls[method].append((args, kwargs))
147 return 0
149 def bbox(self):
150 self.finish()
151 return canvas.canvas.bbox(self)
153 def registerPS(self, registry):
154 self.finish()
155 canvas.canvas.registerPS(self, registry)
157 def registerPDF(self, registry):
158 self.finish()
159 canvas.canvas.registerPDF(self, registry)
161 def processPS(self, file, writer, context, registry, bbox):
162 self.finish()
163 canvas.canvas.processPS(self, file, writer, context, registry, bbox)
165 def processPDF(self, file, writer, context, registry, bbox):
166 self.finish()
167 canvas.canvas.processPDF(self, file, writer, context, registry, bbox)
169 def plot(self, data, styles=None, rangewarning=1):
170 if self.didranges and rangewarning:
171 warnings.warn("axes ranges have already been analysed; no further adjustments will be performed")
172 if self.didstyles:
173 raise RuntimeError("can't plot further data after dostyles() has been executed")
174 singledata = 0
175 try:
176 for d in data:
177 pass
178 except:
179 usedata = [data]
180 singledata = 1
181 else:
182 usedata = data
183 if styles is None:
184 for d in usedata:
185 if styles is None:
186 styles = d.defaultstyles
187 elif styles != d.defaultstyles:
188 raise RuntimeError("defaultstyles differ")
189 plotitems = []
190 for d in usedata:
191 plotitems.append(plotitem(self, d, styles))
192 self.plotitems.extend(plotitems)
193 if self.didranges:
194 for aplotitem in plotitems:
195 aplotitem.makedynamicdata(self)
196 if singledata:
197 return plotitems[0]
198 else:
199 return plotitems
201 def doranges(self):
202 if self.did(self.doranges):
203 return
204 for plotitem in self.plotitems:
205 plotitem.adjustaxesstatic(self)
206 for plotitem in self.plotitems:
207 plotitem.makedynamicdata(self)
208 for plotitem in self.plotitems:
209 plotitem.adjustaxesdynamic(self)
210 self.didranges = 1
212 def doaxiscreate(self, axisname):
213 if self.did(self.doaxiscreate, axisname):
214 return
215 self.doaxispositioner(axisname)
216 self.axes[axisname].create()
218 def dolayout(self):
219 raise NotImplementedError
221 def dobackground(self):
222 pass
224 def doaxes(self):
225 raise NotImplementedError
227 def dostyles(self):
228 if self.did(self.dostyles):
229 return
230 self.dolayout()
231 self.dobackground()
233 # count the usage of styles and perform selects
234 styletotal = {}
235 def stylesid(styles):
236 return ":".join([str(id(style)) for style in styles])
237 for plotitem in self.plotitems:
238 try:
239 styletotal[stylesid(plotitem.styles)] += 1
240 except:
241 styletotal[stylesid(plotitem.styles)] = 1
242 styleindex = {}
243 for plotitem in self.plotitems:
244 try:
245 styleindex[stylesid(plotitem.styles)] += 1
246 except:
247 styleindex[stylesid(plotitem.styles)] = 0
248 plotitem.selectstyles(self, styleindex[stylesid(plotitem.styles)],
249 styletotal[stylesid(plotitem.styles)])
251 self.didstyles = 1
253 def doplot(self, plotitem):
254 if self.did(self.doplot, plotitem):
255 return
256 self.dostyles()
257 plotitem.draw(self)
259 def dodata(self):
260 for plotitem in self.plotitems:
261 self.doplot(plotitem)
263 def dokey(self):
264 raise NotImplementedError
266 def finish(self):
267 self.dobackground()
268 self.doaxes()
269 self.dodata()
270 self.dokey()
273 class graphxy(graph):
275 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
276 key=None, backgroundattrs=None, axesdist=0.8*unit.v_cm,
277 xaxisat=None, yaxisat=None, **axes):
278 graph.__init__(self)
280 self.xpos = xpos
281 self.ypos = ypos
282 self.xpos_pt = unit.topt(self.xpos)
283 self.ypos_pt = unit.topt(self.ypos)
284 self.xaxisat = xaxisat
285 self.yaxisat = yaxisat
286 self.key = key
287 self.backgroundattrs = backgroundattrs
288 self.axesdist_pt = unit.topt(axesdist)
290 self.width = width
291 self.height = height
292 if width is None:
293 if height is None:
294 raise ValueError("specify width and/or height")
295 else:
296 self.width = ratio * self.height
297 elif height is None:
298 self.height = (1.0/ratio) * self.width
299 self.width_pt = unit.topt(self.width)
300 self.height_pt = unit.topt(self.height)
302 for axisname, aaxis in axes.items():
303 if aaxis is not None:
304 if not isinstance(aaxis, axis.linkedaxis):
305 self.axes[axisname] = axis.anchoredaxis(aaxis, self.texrunner, axisname)
306 else:
307 self.axes[axisname] = aaxis
308 for axisname, axisat in [("x", xaxisat), ("y", yaxisat)]:
309 okey = axisname + "2"
310 if not axes.has_key(axisname):
311 if not axes.has_key(okey) or axes[okey] is None:
312 self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.texrunner, axisname)
313 if not axes.has_key(okey):
314 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
315 else:
316 self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
317 elif not axes.has_key(okey) and axisat is None:
318 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
320 if self.axes.has_key("x"):
321 self.xbasepath = self.axes["x"].basepath
322 self.xvbasepath = self.axes["x"].vbasepath
323 self.xgridpath = self.axes["x"].gridpath
324 self.xtickpoint_pt = self.axes["x"].tickpoint_pt
325 self.xtickpoint = self.axes["x"].tickpoint
326 self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
327 self.xvtickpoint = self.axes["x"].tickpoint
328 self.xtickdirection = self.axes["x"].tickdirection
329 self.xvtickdirection = self.axes["x"].vtickdirection
331 if self.axes.has_key("y"):
332 self.ybasepath = self.axes["y"].basepath
333 self.yvbasepath = self.axes["y"].vbasepath
334 self.ygridpath = self.axes["y"].gridpath
335 self.ytickpoint_pt = self.axes["y"].tickpoint_pt
336 self.ytickpoint = self.axes["y"].tickpoint
337 self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
338 self.yvtickpoint = self.axes["y"].vtickpoint
339 self.ytickdirection = self.axes["y"].tickdirection
340 self.yvtickdirection = self.axes["y"].vtickdirection
342 self.axesnames = ([], [])
343 for axisname, aaxis in self.axes.items():
344 if axisname[0] not in "xy" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
345 axisname[1:] == "1")):
346 raise ValueError("invalid axis name")
347 if axisname[0] == "x":
348 self.axesnames[0].append(axisname)
349 else:
350 self.axesnames[1].append(axisname)
351 aaxis.setcreatecall(self.doaxiscreate, axisname)
354 def pos_pt(self, x, y, xaxis=None, yaxis=None):
355 if xaxis is None:
356 xaxis = self.axes["x"]
357 if yaxis is None:
358 yaxis = self.axes["y"]
359 return (self.xpos_pt + xaxis.convert(x)*self.width_pt,
360 self.ypos_pt + yaxis.convert(y)*self.height_pt)
362 def pos(self, x, y, xaxis=None, yaxis=None):
363 if xaxis is None:
364 xaxis = self.axes["x"]
365 if yaxis is None:
366 yaxis = self.axes["y"]
367 return (self.xpos + xaxis.convert(x)*self.width,
368 self.ypos + yaxis.convert(y)*self.height)
370 def vpos_pt(self, vx, vy):
371 return (self.xpos_pt + vx*self.width_pt,
372 self.ypos_pt + vy*self.height_pt)
374 def vpos(self, vx, vy):
375 return (self.xpos + vx*self.width,
376 self.ypos + vy*self.height)
378 def vzindex(self, vx, vy):
379 return 0
381 def vangle(self, vx1, vy1, vx2, vy2, vx3, vy3):
382 return 1
384 def vgeodesic(self, vx1, vy1, vx2, vy2):
385 """returns a geodesic path between two points in graph coordinates"""
386 return path.line_pt(self.xpos_pt + vx1*self.width_pt,
387 self.ypos_pt + vy1*self.height_pt,
388 self.xpos_pt + vx2*self.width_pt,
389 self.ypos_pt + vy2*self.height_pt)
391 def vgeodesic_el(self, vx1, vy1, vx2, vy2):
392 """returns a geodesic path element between two points in graph coordinates"""
393 return path.lineto_pt(self.xpos_pt + vx2*self.width_pt,
394 self.ypos_pt + vy2*self.height_pt)
396 def vcap_pt(self, coordinate, length_pt, vx, vy):
397 """returns an error cap path for a given coordinate, lengths and
398 point in graph coordinates"""
399 if coordinate == 0:
400 return path.line_pt(self.xpos_pt + vx*self.width_pt - 0.5*length_pt,
401 self.ypos_pt + vy*self.height_pt,
402 self.xpos_pt + vx*self.width_pt + 0.5*length_pt,
403 self.ypos_pt + vy*self.height_pt)
404 elif coordinate == 1:
405 return path.line_pt(self.xpos_pt + vx*self.width_pt,
406 self.ypos_pt + vy*self.height_pt - 0.5*length_pt,
407 self.xpos_pt + vx*self.width_pt,
408 self.ypos_pt + vy*self.height_pt + 0.5*length_pt)
409 else:
410 raise ValueError("direction invalid")
412 def xvgridpath(self, vx):
413 return path.line_pt(self.xpos_pt + vx*self.width_pt, self.ypos_pt,
414 self.xpos_pt + vx*self.width_pt, self.ypos_pt + self.height_pt)
416 def yvgridpath(self, vy):
417 return path.line_pt(self.xpos_pt, self.ypos_pt + vy*self.height_pt,
418 self.xpos_pt + self.width_pt, self.ypos_pt + vy*self.height_pt)
420 def axistrafo(self, axis, t):
421 c = canvas.canvas([t])
422 c.insert(axis.canvas)
423 axis.canvas = c
425 def axisatv(self, axis, v):
426 if axis.positioner.fixtickdirection[0]:
427 # it is a y-axis
428 self.axistrafo(axis, trafo.translate_pt(self.xpos_pt + v*self.width_pt - axis.positioner.x1_pt, 0))
429 else:
430 # it is an x-axis
431 self.axistrafo(axis, trafo.translate_pt(0, self.ypos_pt + v*self.height_pt - axis.positioner.y1_pt))
433 def doaxispositioner(self, axisname):
434 if self.did(self.doaxispositioner, axisname):
435 return
436 self.doranges()
437 if axisname == "x":
438 self.axes["x"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
439 self.xpos_pt + self.width_pt, self.ypos_pt,
440 (0, 1), self.xvgridpath))
441 elif axisname == "x2":
442 self.axes["x2"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt + self.height_pt,
443 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
444 (0, -1), self.xvgridpath))
445 elif axisname == "y":
446 self.axes["y"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
447 self.xpos_pt, self.ypos_pt + self.height_pt,
448 (1, 0), self.yvgridpath))
449 elif axisname == "y2":
450 self.axes["y2"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt + self.width_pt, self.ypos_pt,
451 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
452 (-1, 0), self.yvgridpath))
453 else:
454 if axisname[1:] == "3":
455 dependsonaxisname = axisname[0]
456 else:
457 dependsonaxisname = "%s%d" % (axisname[0], int(axisname[1:]) - 2)
458 self.doaxiscreate(dependsonaxisname)
459 sign = 2*(int(axisname[1:]) % 2) - 1
460 if axisname[0] == "x":
461 y_pt = self.axes[dependsonaxisname].positioner.y1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
462 self.axes[axisname].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, y_pt,
463 self.xpos_pt + self.width_pt, y_pt,
464 (0, sign), self.xvgridpath))
465 else:
466 x_pt = self.axes[dependsonaxisname].positioner.x1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
467 self.axes[axisname].setpositioner(positioner.lineaxispos_pt(x_pt, self.ypos_pt,
468 x_pt, self.ypos_pt + self.height_pt,
469 (sign, 0), self.yvgridpath))
471 def dolayout(self):
472 if self.did(self.dolayout):
473 return
474 for axisname in self.axes.keys():
475 self.doaxiscreate(axisname)
476 if self.xaxisat is not None:
477 self.axisatv(self.axes["x"], self.axes["y"].convert(self.xaxisat))
478 if self.yaxisat is not None:
479 self.axisatv(self.axes["y"], self.axes["x"].convert(self.yaxisat))
481 def dobackground(self):
482 if self.did(self.dobackground):
483 return
484 if self.backgroundattrs is not None:
485 self.draw(path.rect_pt(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt),
486 self.backgroundattrs)
488 def doaxes(self):
489 if self.did(self.doaxes):
490 return
491 self.dolayout()
492 self.dobackground()
493 for axis in self.axes.values():
494 self.insert(axis.canvas)
496 def dokey(self):
497 if self.did(self.dokey):
498 return
499 self.dobackground()
500 self.dostyles()
501 if self.key is not None:
502 c = self.key.paint(self.plotitems)
503 bbox = c.bbox()
504 def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
505 ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
506 cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
507 return ppos-cpos
508 if bbox:
509 x = parentchildalign(self.xpos_pt, self.xpos_pt+self.width_pt,
510 bbox.llx_pt, bbox.urx_pt,
511 self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
512 y = parentchildalign(self.ypos_pt, self.ypos_pt+self.height_pt,
513 bbox.lly_pt, bbox.ury_pt,
514 self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
515 self.insert(c, [trafo.translate_pt(x, y)])
518 class graphxyz(graphxy):
520 class central:
522 def __init__(self, distance, phi, theta, anglefactor=math.pi/180):
523 phi *= anglefactor
524 theta *= anglefactor
525 self.distance = distance
527 self.a = (-math.sin(phi), math.cos(phi), 0)
528 self.b = (-math.cos(phi)*math.sin(theta),
529 -math.sin(phi)*math.sin(theta),
530 math.cos(theta))
531 self.eye = (distance*math.cos(phi)*math.cos(theta),
532 distance*math.sin(phi)*math.cos(theta),
533 distance*math.sin(theta))
535 def point(self, x, y, z):
536 d0 = (self.a[0]*self.b[1]*(z-self.eye[2])
537 + self.a[2]*self.b[0]*(y-self.eye[1])
538 + self.a[1]*self.b[2]*(x-self.eye[0])
539 - self.a[2]*self.b[1]*(x-self.eye[0])
540 - self.a[0]*self.b[2]*(y-self.eye[1])
541 - self.a[1]*self.b[0]*(z-self.eye[2]))
542 da = (self.eye[0]*self.b[1]*(z-self.eye[2])
543 + self.eye[2]*self.b[0]*(y-self.eye[1])
544 + self.eye[1]*self.b[2]*(x-self.eye[0])
545 - self.eye[2]*self.b[1]*(x-self.eye[0])
546 - self.eye[0]*self.b[2]*(y-self.eye[1])
547 - self.eye[1]*self.b[0]*(z-self.eye[2]))
548 db = (self.a[0]*self.eye[1]*(z-self.eye[2])
549 + self.a[2]*self.eye[0]*(y-self.eye[1])
550 + self.a[1]*self.eye[2]*(x-self.eye[0])
551 - self.a[2]*self.eye[1]*(x-self.eye[0])
552 - self.a[0]*self.eye[2]*(y-self.eye[1])
553 - self.a[1]*self.eye[0]*(z-self.eye[2]))
554 return da/d0, db/d0
556 def zindex(self, x, y, z):
557 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
559 def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
560 sx = (x1-self.eye[0])
561 sy = (y1-self.eye[1])
562 sz = (z1-self.eye[2])
563 nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
564 ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
565 nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
566 return (sx*nx+sy*ny+sz*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)/math.sqrt(sx*sx+sy*sy+sz*sz)
569 class parallel:
571 def __init__(self, phi, theta, anglefactor=math.pi/180):
572 phi *= anglefactor
573 theta *= anglefactor
575 self.a = (-math.sin(phi), math.cos(phi), 0)
576 self.b = (-math.cos(phi)*math.sin(theta),
577 -math.sin(phi)*math.sin(theta),
578 math.cos(theta))
579 self.c = (-math.cos(phi)*math.cos(theta),
580 -math.sin(phi)*math.cos(theta),
581 -math.sin(theta))
583 def point(self, x, y, z):
584 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
586 def zindex(self, x, y, z):
587 return self.c[0]*x+self.c[1]*y+self.c[2]*z
589 def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
590 nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
591 ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
592 nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
593 return (self.c[0]*nx+self.c[1]*ny+self.c[2]*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)
596 def __init__(self, xpos=0, ypos=0, size=None,
597 xscale=1, yscale=1, zscale=1/goldenmean,
598 projector=central(10, -30, 30), key=None,
599 **axes):
600 graph.__init__(self)
602 self.xpos = xpos
603 self.ypos = ypos
604 self.size = size
605 self.xpos_pt = unit.topt(xpos)
606 self.ypos_pt = unit.topt(ypos)
607 self.size_pt = unit.topt(size)
608 self.xscale = xscale
609 self.yscale = yscale
610 self.zscale = zscale
611 self.projector = projector
612 self.key = key
614 self.xorder = projector.zindex(0, -1, 0) > projector.zindex(0, 1, 0) and 1 or 0
615 self.yorder = projector.zindex(-1, 0, 0) > projector.zindex(1, 0, 0) and 1 or 0
616 self.zindexscale = math.sqrt(xscale*xscale+yscale*yscale+zscale*zscale)
618 for axisname, aaxis in axes.items():
619 if aaxis is not None:
620 if not isinstance(aaxis, axis.linkedaxis):
621 self.axes[axisname] = axis.anchoredaxis(aaxis, self.texrunner, axisname)
622 else:
623 self.axes[axisname] = aaxis
624 for axisname in ["x", "y"]:
625 okey = axisname + "2"
626 if not axes.has_key(axisname):
627 if not axes.has_key(okey) or axes[okey] is None:
628 self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.texrunner, axisname)
629 if not axes.has_key(okey):
630 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
631 else:
632 self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
633 if not axes.has_key("z"):
634 self.axes["z"] = axis.anchoredaxis(axis.linear(), self.texrunner, axisname)
636 if self.axes.has_key("x"):
637 self.xbasepath = self.axes["x"].basepath
638 self.xvbasepath = self.axes["x"].vbasepath
639 self.xgridpath = self.axes["x"].gridpath
640 self.xtickpoint_pt = self.axes["x"].tickpoint_pt
641 self.xtickpoint = self.axes["x"].tickpoint
642 self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
643 self.xvtickpoint = self.axes["x"].tickpoint
644 self.xtickdirection = self.axes["x"].tickdirection
645 self.xvtickdirection = self.axes["x"].vtickdirection
647 if self.axes.has_key("y"):
648 self.ybasepath = self.axes["y"].basepath
649 self.yvbasepath = self.axes["y"].vbasepath
650 self.ygridpath = self.axes["y"].gridpath
651 self.ytickpoint_pt = self.axes["y"].tickpoint_pt
652 self.ytickpoint = self.axes["y"].tickpoint
653 self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
654 self.yvtickpoint = self.axes["y"].vtickpoint
655 self.ytickdirection = self.axes["y"].tickdirection
656 self.yvtickdirection = self.axes["y"].vtickdirection
658 if self.axes.has_key("z"):
659 self.zbasepath = self.axes["z"].basepath
660 self.zvbasepath = self.axes["z"].vbasepath
661 self.zgridpath = self.axes["z"].gridpath
662 self.ztickpoint_pt = self.axes["z"].tickpoint_pt
663 self.ztickpoint = self.axes["z"].tickpoint
664 self.zvtickpoint_pt = self.axes["z"].vtickpoint
665 self.zvtickpoint = self.axes["z"].vtickpoint
666 self.ztickdirection = self.axes["z"].tickdirection
667 self.zvtickdirection = self.axes["z"].vtickdirection
669 self.axesnames = ([], [], [])
670 for axisname, aaxis in self.axes.items():
671 if axisname[0] not in "xyz" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
672 axisname[1:] == "1")):
673 raise ValueError("invalid axis name")
674 if axisname[0] == "x":
675 self.axesnames[0].append(axisname)
676 elif axisname[0] == "y":
677 self.axesnames[1].append(axisname)
678 else:
679 self.axesnames[2].append(axisname)
680 aaxis.setcreatecall(self.doaxiscreate, axisname)
682 def pos_pt(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
683 if xaxis is None:
684 xaxis = self.axes["x"]
685 if yaxis is None:
686 yaxis = self.axes["y"]
687 if zaxis is None:
688 zaxis = self.axes["z"]
689 return self.vpos_pt(xaxis.convert(x), yaxis.convert(y), zaxis.convert(y))
691 def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
692 if xaxis is None:
693 xaxis = self.axes["x"]
694 if yaxis is None:
695 yaxis = self.axes["y"]
696 if zaxis is None:
697 zaxis = self.axes["z"]
698 return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(y))
700 def vpos_pt(self, vx, vy, vz):
701 x, y = self.projector.point(2*self.xscale*(vx - 0.5),
702 2*self.yscale*(vy - 0.5),
703 2*self.zscale*(vz - 0.5))
704 return self.xpos_pt+x*self.size_pt, self.ypos_pt+y*self.size_pt
706 def vpos(self, vx, vy, vz):
707 x, y = self.projector.point(2*self.xscale*(vx - 0.5),
708 2*self.yscale*(vy - 0.5),
709 2*self.zscale*(vz - 0.5))
710 return self.xpos+x*self.size, self.ypos+y*self.size
712 def vzindex(self, vx, vy, vz):
713 return self.projector.zindex(2*self.xscale*(vx - 0.5),
714 2*self.yscale*(vy - 0.5),
715 2*self.zscale*(vz - 0.5))/self.zindexscale
717 def vangle(self, vx1, vy1, vz1, vx2, vy2, vz2, vx3, vy3, vz3):
718 return self.projector.angle(2*self.xscale*(vx1 - 0.5),
719 2*self.yscale*(vy1 - 0.5),
720 2*self.zscale*(vz1 - 0.5),
721 2*self.xscale*(vx2 - 0.5),
722 2*self.yscale*(vy2 - 0.5),
723 2*self.zscale*(vz2 - 0.5),
724 2*self.xscale*(vx3 - 0.5),
725 2*self.yscale*(vy3 - 0.5),
726 2*self.zscale*(vz3 - 0.5))
728 def vgeodesic(self, vx1, vy1, vz1, vx2, vy2, vz2):
729 """returns a geodesic path between two points in graph coordinates"""
730 return path.line_pt(*(self.vpos_pt(vx1, vy1, vz1) + self.vpos_pt(vx2, vy2, vz2)))
732 def vgeodesic_el(self, vx1, vy1, vz1, vx2, vy2, vz2):
733 """returns a geodesic path element between two points in graph coordinates"""
734 return path.lineto_pt(*(self.vpos_pt(vx1, vy1, vz1) + self.vpos_pt(vx2, vy2, vz2)))
736 def vcap_pt(self, coordinate, length_pt, vx, vy, vz):
737 """returns an error cap path for a given coordinate, lengths and
738 point in graph coordinates"""
739 if coordinate == 0:
740 return self.vgeodesic(vx-0.5*length_pt/self.size_pt, vy, vz, vx+0.5*length_pt/self.size_pt, vy, vz)
741 elif coordinate == 1:
742 return self.vgeodesic(vx, vy-0.5*length_pt/self.size_pt, vz, vx, vy+0.5*length_pt/self.size_pt, vz)
743 elif coordinate == 2:
744 return self.vgeodesic(vx, vy, vz-0.5*length_pt/self.size_pt, vx, vy, vz+0.5*length_pt/self.size_pt)
745 else:
746 raise ValueError("direction invalid")
748 def xvtickdirection(self, vx):
749 if self.xorder:
750 x1_pt, y1_pt = self.vpos_pt(vx, 1, 0)
751 x2_pt, y2_pt = self.vpos_pt(vx, 0, 0)
752 else:
753 x1_pt, y1_pt = self.vpos_pt(vx, 0, 0)
754 x2_pt, y2_pt = self.vpos_pt(vx, 1, 0)
755 dx_pt = x2_pt - x1_pt
756 dy_pt = y2_pt - y1_pt
757 norm = math.hypot(dx_pt, dy_pt)
758 return dx_pt/norm, dy_pt/norm
760 def yvtickdirection(self, vy):
761 if self.yorder:
762 x1_pt, y1_pt = self.vpos_pt(1, vy, 0)
763 x2_pt, y2_pt = self.vpos_pt(0, vy, 0)
764 else:
765 x1_pt, y1_pt = self.vpos_pt(0, vy, 0)
766 x2_pt, y2_pt = self.vpos_pt(1, vy, 0)
767 dx_pt = x2_pt - x1_pt
768 dy_pt = y2_pt - y1_pt
769 norm = math.hypot(dx_pt, dy_pt)
770 return dx_pt/norm, dy_pt/norm
772 def vtickdirection(self, vx1, vy1, vz1, vx2, vy2, vz2):
773 x1_pt, y1_pt = self.vpos_pt(vx1, vy1, vz1)
774 x2_pt, y2_pt = self.vpos_pt(vx2, vy2, vz2)
775 dx_pt = x2_pt - x1_pt
776 dy_pt = y2_pt - y1_pt
777 norm = math.hypot(dx_pt, dy_pt)
778 return dx_pt/norm, dy_pt/norm
780 def xvgridpath(self, vx):
781 return path.path(path.moveto_pt(*self.vpos_pt(vx, 0, 0)),
782 path.lineto_pt(*self.vpos_pt(vx, 1, 0)),
783 path.lineto_pt(*self.vpos_pt(vx, 1, 1)),
784 path.lineto_pt(*self.vpos_pt(vx, 0, 1)),
785 path.closepath())
787 def yvgridpath(self, vy):
788 return path.path(path.moveto_pt(*self.vpos_pt(0, vy, 0)),
789 path.lineto_pt(*self.vpos_pt(1, vy, 0)),
790 path.lineto_pt(*self.vpos_pt(1, vy, 1)),
791 path.lineto_pt(*self.vpos_pt(0, vy, 1)),
792 path.closepath())
794 def zvgridpath(self, vz):
795 return path.path(path.moveto_pt(*self.vpos_pt(0, 0, vz)),
796 path.lineto_pt(*self.vpos_pt(1, 0, vz)),
797 path.lineto_pt(*self.vpos_pt(1, 1, vz)),
798 path.lineto_pt(*self.vpos_pt(0, 1, vz)),
799 path.closepath())
801 def doaxispositioner(self, axisname):
802 if self.did(self.doaxispositioner, axisname):
803 return
804 self.doranges()
805 if axisname == "x":
806 self.axes["x"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 0),
807 lambda vx: self.vtickdirection(vx, self.xorder, 0, vx, 1-self.xorder, 0),
808 self.xvgridpath))
809 elif axisname == "x2":
810 self.axes["x2"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 0),
811 lambda vx: self.vtickdirection(vx, 1-self.xorder, 0, vx, self.xorder, 0),
812 self.xvgridpath))
813 elif axisname == "x3":
814 self.axes["x3"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 1),
815 lambda vx: self.vtickdirection(vx, self.xorder, 1, vx, 1-self.xorder, 1),
816 self.xvgridpath))
817 elif axisname == "x4":
818 self.axes["x4"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 1),
819 lambda vx: self.vtickdirection(vx, 1-self.xorder, 1, vx, self.xorder, 1),
820 self.xvgridpath))
821 elif axisname == "y":
822 self.axes["y"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 0),
823 lambda vy: self.vtickdirection(self.yorder, vy, 0, 1-self.yorder, vy, 0),
824 self.yvgridpath))
825 elif axisname == "y2":
826 self.axes["y2"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 0),
827 lambda vy: self.vtickdirection(1-self.yorder, vy, 0, self.yorder, vy, 0),
828 self.yvgridpath))
829 elif axisname == "y3":
830 self.axes["y3"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 1),
831 lambda vy: self.vtickdirection(self.yorder, vy, 1, 1-self.yorder, vy, 1),
832 self.yvgridpath))
833 elif axisname == "y4":
834 self.axes["y4"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 1),
835 lambda vy: self.vtickdirection(1-self.yorder, vy, 1, self.yorder, vy, 1),
836 self.yvgridpath))
837 elif axisname == "z":
838 self.axes["z"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 0, vz),
839 lambda vz: self.vtickdirection(0, 0, vz, 1, 1, vz),
840 self.zvgridpath))
841 elif axisname == "z2":
842 self.axes["z2"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 0, vz),
843 lambda vz: self.vtickdirection(1, 0, vz, 0, 1, vz),
844 self.zvgridpath))
845 elif axisname == "z3":
846 self.axes["z3"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 1, vz),
847 lambda vz: self.vtickdirection(0, 1, vz, 1, 0, vz),
848 self.zvgridpath))
849 elif axisname == "z4":
850 self.axes["z4"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 0, vz),
851 lambda vz: self.vtickdirection(1, 1, vz, 0, 0, vz),
852 self.zvgridpath))
853 else:
854 raise NotImplementedError("4 axis per dimension supported only")
856 def dolayout(self):
857 if self.did(self.dolayout):
858 return
859 for axisname in self.axes.keys():
860 self.doaxiscreate(axisname)
862 def dobackground(self):
863 if self.did(self.dobackground):
864 return
866 def doaxes(self):
867 if self.did(self.doaxes):
868 return
869 self.dolayout()
870 self.dobackground()
871 for axis in self.axes.values():
872 self.insert(axis.canvas)
874 def dokey(self):
875 if self.did(self.dokey):
876 return
877 self.dobackground()
878 self.dostyles()
879 if self.key is not None:
880 c = self.key.paint(self.plotitems)
881 bbox = c.bbox()
882 def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
883 ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
884 cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
885 return ppos-cpos
886 if bbox:
887 x = parentchildalign(self.xpos_pt, self.xpos_pt+self.size_pt,
888 bbox.llx_pt, bbox.urx_pt,
889 self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
890 y = parentchildalign(self.ypos_pt, self.ypos_pt+self.size_pt,
891 bbox.lly_pt, bbox.ury_pt,
892 self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
893 self.insert(c, [trafo.translate_pt(x, y)])