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