3d bar graphs (without occlusion)
[PyX/mjg.git] / pyx / graph / graph.py
blob5f7e48512a4e402ef811b30bb19ff4523c6279c1
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.keyitems = []
137 self._calls = {}
138 self.didranges = 0
139 self.didstyles = 0
141 def did(self, method, *args, **kwargs):
142 if not self._calls.has_key(method):
143 self._calls[method] = []
144 for callargs in self._calls[method]:
145 if callargs == (args, kwargs):
146 return 1
147 self._calls[method].append((args, kwargs))
148 return 0
150 def bbox(self):
151 self.finish()
152 return canvas.canvas.bbox(self)
154 def registerPS(self, registry):
155 self.finish()
156 canvas.canvas.registerPS(self, registry)
158 def registerPDF(self, registry):
159 self.finish()
160 canvas.canvas.registerPDF(self, registry)
162 def processPS(self, file, writer, context, registry, bbox):
163 self.finish()
164 canvas.canvas.processPS(self, file, writer, context, registry, bbox)
166 def processPDF(self, file, writer, context, registry, bbox):
167 self.finish()
168 canvas.canvas.processPDF(self, file, writer, context, registry, bbox)
170 def plot(self, data, styles=None, rangewarning=1):
171 if self.didranges and rangewarning:
172 warnings.warn("axes ranges have already been analysed; no further adjustments will be performed")
173 if self.didstyles:
174 raise RuntimeError("can't plot further data after dostyles() has been executed")
175 singledata = 0
176 try:
177 for d in data:
178 pass
179 except:
180 usedata = [data]
181 singledata = 1
182 else:
183 usedata = data
184 if styles is None:
185 for d in usedata:
186 if styles is None:
187 styles = d.defaultstyles
188 elif styles != d.defaultstyles:
189 raise RuntimeError("defaultstyles differ")
190 plotitems = []
191 for d in usedata:
192 plotitems.append(plotitem(self, d, styles))
193 self.plotitems.extend(plotitems)
194 if self.didranges:
195 for aplotitem in plotitems:
196 aplotitem.makedynamicdata(self)
197 if singledata:
198 return plotitems[0]
199 else:
200 return plotitems
202 def doranges(self):
203 if self.did(self.doranges):
204 return
205 for plotitem in self.plotitems:
206 plotitem.adjustaxesstatic(self)
207 for plotitem in self.plotitems:
208 plotitem.makedynamicdata(self)
209 for plotitem in self.plotitems:
210 plotitem.adjustaxesdynamic(self)
211 self.didranges = 1
213 def doaxiscreate(self, axisname):
214 if self.did(self.doaxiscreate, axisname):
215 return
216 self.doaxispositioner(axisname)
217 self.axes[axisname].create()
219 def dolayout(self):
220 raise NotImplementedError
222 def dobackground(self):
223 pass
225 def doaxes(self):
226 raise NotImplementedError
228 def dostyles(self):
229 if self.did(self.dostyles):
230 return
231 self.dolayout()
232 self.dobackground()
234 # count the usage of styles and perform selects
235 styletotal = {}
236 def stylesid(styles):
237 return ":".join([str(id(style)) for style in styles])
238 for plotitem in self.plotitems:
239 try:
240 styletotal[stylesid(plotitem.styles)] += 1
241 except:
242 styletotal[stylesid(plotitem.styles)] = 1
243 styleindex = {}
244 for plotitem in self.plotitems:
245 try:
246 styleindex[stylesid(plotitem.styles)] += 1
247 except:
248 styleindex[stylesid(plotitem.styles)] = 0
249 plotitem.selectstyles(self, styleindex[stylesid(plotitem.styles)],
250 styletotal[stylesid(plotitem.styles)])
252 self.didstyles = 1
254 def doplotitem(self, plotitem):
255 if self.did(self.doplotitem, plotitem):
256 return
257 self.dostyles()
258 plotitem.draw(self)
260 def doplot(self):
261 for plotitem in self.plotitems:
262 self.doplotitem(plotitem)
264 def dodata(self):
265 warnings.warn("dodata() has been deprecated. Use doplot() instead.")
266 self.doplot()
268 def dokeyitem(self, plotitem):
269 if self.did(self.dokeyitem, plotitem):
270 return
271 self.dostyles()
272 if plotitem.title is not None:
273 self.keyitems.append(plotitem)
275 def dokey(self):
276 raise NotImplementedError
278 def finish(self):
279 self.dobackground()
280 self.doaxes()
281 self.doplot()
282 self.dokey()
285 class graphxy(graph):
287 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
288 key=None, backgroundattrs=None, axesdist=0.8*unit.v_cm,
289 xaxisat=None, yaxisat=None, **axes):
290 graph.__init__(self)
292 self.xpos = xpos
293 self.ypos = ypos
294 self.xpos_pt = unit.topt(self.xpos)
295 self.ypos_pt = unit.topt(self.ypos)
296 self.xaxisat = xaxisat
297 self.yaxisat = yaxisat
298 self.key = key
299 self.backgroundattrs = backgroundattrs
300 self.axesdist_pt = unit.topt(axesdist)
302 self.width = width
303 self.height = height
304 if width is None:
305 if height is None:
306 raise ValueError("specify width and/or height")
307 else:
308 self.width = ratio * self.height
309 elif height is None:
310 self.height = (1.0/ratio) * self.width
311 self.width_pt = unit.topt(self.width)
312 self.height_pt = unit.topt(self.height)
314 for axisname, aaxis in axes.items():
315 if aaxis is not None:
316 if not isinstance(aaxis, axis.linkedaxis):
317 self.axes[axisname] = axis.anchoredaxis(aaxis, self.texrunner, axisname)
318 else:
319 self.axes[axisname] = aaxis
320 for axisname, axisat in [("x", xaxisat), ("y", yaxisat)]:
321 okey = axisname + "2"
322 if not axes.has_key(axisname):
323 if not axes.has_key(okey) or axes[okey] is None:
324 self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.texrunner, axisname)
325 if not axes.has_key(okey):
326 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
327 else:
328 self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
329 elif not axes.has_key(okey) and axisat is None:
330 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
332 if self.axes.has_key("x"):
333 self.xbasepath = self.axes["x"].basepath
334 self.xvbasepath = self.axes["x"].vbasepath
335 self.xgridpath = self.axes["x"].gridpath
336 self.xtickpoint_pt = self.axes["x"].tickpoint_pt
337 self.xtickpoint = self.axes["x"].tickpoint
338 self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
339 self.xvtickpoint = self.axes["x"].tickpoint
340 self.xtickdirection = self.axes["x"].tickdirection
341 self.xvtickdirection = self.axes["x"].vtickdirection
343 if self.axes.has_key("y"):
344 self.ybasepath = self.axes["y"].basepath
345 self.yvbasepath = self.axes["y"].vbasepath
346 self.ygridpath = self.axes["y"].gridpath
347 self.ytickpoint_pt = self.axes["y"].tickpoint_pt
348 self.ytickpoint = self.axes["y"].tickpoint
349 self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
350 self.yvtickpoint = self.axes["y"].vtickpoint
351 self.ytickdirection = self.axes["y"].tickdirection
352 self.yvtickdirection = self.axes["y"].vtickdirection
354 self.axesnames = ([], [])
355 for axisname, aaxis in self.axes.items():
356 if axisname[0] not in "xy" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
357 axisname[1:] == "1")):
358 raise ValueError("invalid axis name")
359 if axisname[0] == "x":
360 self.axesnames[0].append(axisname)
361 else:
362 self.axesnames[1].append(axisname)
363 aaxis.setcreatecall(self.doaxiscreate, axisname)
366 def pos_pt(self, x, y, xaxis=None, yaxis=None):
367 if xaxis is None:
368 xaxis = self.axes["x"]
369 if yaxis is None:
370 yaxis = self.axes["y"]
371 return (self.xpos_pt + xaxis.convert(x)*self.width_pt,
372 self.ypos_pt + yaxis.convert(y)*self.height_pt)
374 def pos(self, x, y, xaxis=None, yaxis=None):
375 if xaxis is None:
376 xaxis = self.axes["x"]
377 if yaxis is None:
378 yaxis = self.axes["y"]
379 return (self.xpos + xaxis.convert(x)*self.width,
380 self.ypos + yaxis.convert(y)*self.height)
382 def vpos_pt(self, vx, vy):
383 return (self.xpos_pt + vx*self.width_pt,
384 self.ypos_pt + vy*self.height_pt)
386 def vpos(self, vx, vy):
387 return (self.xpos + vx*self.width,
388 self.ypos + vy*self.height)
390 def vzindex(self, vx, vy):
391 return 0
393 def vangle(self, vx1, vy1, vx2, vy2, vx3, vy3):
394 return 1
396 def vgeodesic(self, vx1, vy1, vx2, vy2):
397 """returns a geodesic path between two points in graph coordinates"""
398 return path.line_pt(self.xpos_pt + vx1*self.width_pt,
399 self.ypos_pt + vy1*self.height_pt,
400 self.xpos_pt + vx2*self.width_pt,
401 self.ypos_pt + vy2*self.height_pt)
403 def vgeodesic_el(self, vx1, vy1, vx2, vy2):
404 """returns a geodesic path element between two points in graph coordinates"""
405 return path.lineto_pt(self.xpos_pt + vx2*self.width_pt,
406 self.ypos_pt + vy2*self.height_pt)
408 def vcap_pt(self, coordinate, length_pt, vx, vy):
409 """returns an error cap path for a given coordinate, lengths and
410 point in graph coordinates"""
411 if coordinate == 0:
412 return path.line_pt(self.xpos_pt + vx*self.width_pt - 0.5*length_pt,
413 self.ypos_pt + vy*self.height_pt,
414 self.xpos_pt + vx*self.width_pt + 0.5*length_pt,
415 self.ypos_pt + vy*self.height_pt)
416 elif coordinate == 1:
417 return path.line_pt(self.xpos_pt + vx*self.width_pt,
418 self.ypos_pt + vy*self.height_pt - 0.5*length_pt,
419 self.xpos_pt + vx*self.width_pt,
420 self.ypos_pt + vy*self.height_pt + 0.5*length_pt)
421 else:
422 raise ValueError("direction invalid")
424 def xvgridpath(self, vx):
425 return path.line_pt(self.xpos_pt + vx*self.width_pt, self.ypos_pt,
426 self.xpos_pt + vx*self.width_pt, self.ypos_pt + self.height_pt)
428 def yvgridpath(self, vy):
429 return path.line_pt(self.xpos_pt, self.ypos_pt + vy*self.height_pt,
430 self.xpos_pt + self.width_pt, self.ypos_pt + vy*self.height_pt)
432 def axistrafo(self, axis, t):
433 c = canvas.canvas([t])
434 c.insert(axis.canvas)
435 axis.canvas = c
437 def axisatv(self, axis, v):
438 if axis.positioner.fixtickdirection[0]:
439 # it is a y-axis
440 self.axistrafo(axis, trafo.translate_pt(self.xpos_pt + v*self.width_pt - axis.positioner.x1_pt, 0))
441 else:
442 # it is an x-axis
443 self.axistrafo(axis, trafo.translate_pt(0, self.ypos_pt + v*self.height_pt - axis.positioner.y1_pt))
445 def doaxispositioner(self, axisname):
446 if self.did(self.doaxispositioner, axisname):
447 return
448 self.doranges()
449 if axisname == "x":
450 self.axes["x"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
451 self.xpos_pt + self.width_pt, self.ypos_pt,
452 (0, 1), self.xvgridpath))
453 elif axisname == "x2":
454 self.axes["x2"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt + self.height_pt,
455 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
456 (0, -1), self.xvgridpath))
457 elif axisname == "y":
458 self.axes["y"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
459 self.xpos_pt, self.ypos_pt + self.height_pt,
460 (1, 0), self.yvgridpath))
461 elif axisname == "y2":
462 self.axes["y2"].setpositioner(positioner.lineaxispos_pt(self.xpos_pt + self.width_pt, self.ypos_pt,
463 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
464 (-1, 0), self.yvgridpath))
465 else:
466 if axisname[1:] == "3":
467 dependsonaxisname = axisname[0]
468 else:
469 dependsonaxisname = "%s%d" % (axisname[0], int(axisname[1:]) - 2)
470 self.doaxiscreate(dependsonaxisname)
471 sign = 2*(int(axisname[1:]) % 2) - 1
472 if axisname[0] == "x":
473 y_pt = self.axes[dependsonaxisname].positioner.y1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
474 self.axes[axisname].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, y_pt,
475 self.xpos_pt + self.width_pt, y_pt,
476 (0, sign), self.xvgridpath))
477 else:
478 x_pt = self.axes[dependsonaxisname].positioner.x1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
479 self.axes[axisname].setpositioner(positioner.lineaxispos_pt(x_pt, self.ypos_pt,
480 x_pt, self.ypos_pt + self.height_pt,
481 (sign, 0), self.yvgridpath))
483 def dolayout(self):
484 if self.did(self.dolayout):
485 return
486 for axisname in self.axes.keys():
487 self.doaxiscreate(axisname)
488 if self.xaxisat is not None:
489 self.axisatv(self.axes["x"], self.axes["y"].convert(self.xaxisat))
490 if self.yaxisat is not None:
491 self.axisatv(self.axes["y"], self.axes["x"].convert(self.yaxisat))
493 def dobackground(self):
494 if self.did(self.dobackground):
495 return
496 if self.backgroundattrs is not None:
497 self.draw(path.rect_pt(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt),
498 self.backgroundattrs)
500 def doaxes(self):
501 if self.did(self.doaxes):
502 return
503 self.dolayout()
504 self.dobackground()
505 for axis in self.axes.values():
506 self.insert(axis.canvas)
508 def dokey(self):
509 if self.did(self.dokey):
510 return
511 self.dobackground()
512 for plotitem in self.plotitems:
513 self.dokeyitem(plotitem)
514 if self.key is not None:
515 c = self.key.paint(self.keyitems)
516 bbox = c.bbox()
517 def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
518 ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
519 cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
520 return ppos-cpos
521 if bbox:
522 x = parentchildalign(self.xpos_pt, self.xpos_pt+self.width_pt,
523 bbox.llx_pt, bbox.urx_pt,
524 self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
525 y = parentchildalign(self.ypos_pt, self.ypos_pt+self.height_pt,
526 bbox.lly_pt, bbox.ury_pt,
527 self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
528 self.insert(c, [trafo.translate_pt(x, y)])
531 class graphxyz(graphxy):
533 class central:
535 def __init__(self, distance, phi, theta, anglefactor=math.pi/180):
536 phi *= anglefactor
537 theta *= anglefactor
538 self.distance = distance
540 self.a = (-math.sin(phi), math.cos(phi), 0)
541 self.b = (-math.cos(phi)*math.sin(theta),
542 -math.sin(phi)*math.sin(theta),
543 math.cos(theta))
544 self.eye = (distance*math.cos(phi)*math.cos(theta),
545 distance*math.sin(phi)*math.cos(theta),
546 distance*math.sin(theta))
548 def point(self, x, y, z):
549 d0 = (self.a[0]*self.b[1]*(z-self.eye[2])
550 + self.a[2]*self.b[0]*(y-self.eye[1])
551 + self.a[1]*self.b[2]*(x-self.eye[0])
552 - self.a[2]*self.b[1]*(x-self.eye[0])
553 - self.a[0]*self.b[2]*(y-self.eye[1])
554 - self.a[1]*self.b[0]*(z-self.eye[2]))
555 da = (self.eye[0]*self.b[1]*(z-self.eye[2])
556 + self.eye[2]*self.b[0]*(y-self.eye[1])
557 + self.eye[1]*self.b[2]*(x-self.eye[0])
558 - self.eye[2]*self.b[1]*(x-self.eye[0])
559 - self.eye[0]*self.b[2]*(y-self.eye[1])
560 - self.eye[1]*self.b[0]*(z-self.eye[2]))
561 db = (self.a[0]*self.eye[1]*(z-self.eye[2])
562 + self.a[2]*self.eye[0]*(y-self.eye[1])
563 + self.a[1]*self.eye[2]*(x-self.eye[0])
564 - self.a[2]*self.eye[1]*(x-self.eye[0])
565 - self.a[0]*self.eye[2]*(y-self.eye[1])
566 - self.a[1]*self.eye[0]*(z-self.eye[2]))
567 return da/d0, db/d0
569 def zindex(self, x, y, z):
570 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
572 def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
573 sx = (x1-self.eye[0])
574 sy = (y1-self.eye[1])
575 sz = (z1-self.eye[2])
576 nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
577 ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
578 nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
579 return (sx*nx+sy*ny+sz*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)/math.sqrt(sx*sx+sy*sy+sz*sz)
582 class parallel:
584 def __init__(self, phi, theta, anglefactor=math.pi/180):
585 phi *= anglefactor
586 theta *= anglefactor
588 self.a = (-math.sin(phi), math.cos(phi), 0)
589 self.b = (-math.cos(phi)*math.sin(theta),
590 -math.sin(phi)*math.sin(theta),
591 math.cos(theta))
592 self.c = (-math.cos(phi)*math.cos(theta),
593 -math.sin(phi)*math.cos(theta),
594 -math.sin(theta))
596 def point(self, x, y, z):
597 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
599 def zindex(self, x, y, z):
600 return self.c[0]*x+self.c[1]*y+self.c[2]*z
602 def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
603 nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
604 ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
605 nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
606 return (self.c[0]*nx+self.c[1]*ny+self.c[2]*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)
609 def __init__(self, xpos=0, ypos=0, size=None,
610 xscale=1, yscale=1, zscale=1/goldenmean,
611 projector=central(10, -30, 30), key=None,
612 **axes):
613 graph.__init__(self)
615 self.xpos = xpos
616 self.ypos = ypos
617 self.size = size
618 self.xpos_pt = unit.topt(xpos)
619 self.ypos_pt = unit.topt(ypos)
620 self.size_pt = unit.topt(size)
621 self.xscale = xscale
622 self.yscale = yscale
623 self.zscale = zscale
624 self.projector = projector
625 self.key = key
627 self.xorder = projector.zindex(0, -1, 0) > projector.zindex(0, 1, 0) and 1 or 0
628 self.yorder = projector.zindex(-1, 0, 0) > projector.zindex(1, 0, 0) and 1 or 0
629 self.zindexscale = math.sqrt(xscale*xscale+yscale*yscale+zscale*zscale)
631 for axisname, aaxis in axes.items():
632 if aaxis is not None:
633 if not isinstance(aaxis, axis.linkedaxis):
634 self.axes[axisname] = axis.anchoredaxis(aaxis, self.texrunner, axisname)
635 else:
636 self.axes[axisname] = aaxis
637 for axisname in ["x", "y"]:
638 okey = axisname + "2"
639 if not axes.has_key(axisname):
640 if not axes.has_key(okey) or axes[okey] is None:
641 self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.texrunner, axisname)
642 if not axes.has_key(okey):
643 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
644 else:
645 self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
646 if not axes.has_key("z"):
647 self.axes["z"] = axis.anchoredaxis(axis.linear(), self.texrunner, "z")
649 if self.axes.has_key("x"):
650 self.xbasepath = self.axes["x"].basepath
651 self.xvbasepath = self.axes["x"].vbasepath
652 self.xgridpath = self.axes["x"].gridpath
653 self.xtickpoint_pt = self.axes["x"].tickpoint_pt
654 self.xtickpoint = self.axes["x"].tickpoint
655 self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
656 self.xvtickpoint = self.axes["x"].tickpoint
657 self.xtickdirection = self.axes["x"].tickdirection
658 self.xvtickdirection = self.axes["x"].vtickdirection
660 if self.axes.has_key("y"):
661 self.ybasepath = self.axes["y"].basepath
662 self.yvbasepath = self.axes["y"].vbasepath
663 self.ygridpath = self.axes["y"].gridpath
664 self.ytickpoint_pt = self.axes["y"].tickpoint_pt
665 self.ytickpoint = self.axes["y"].tickpoint
666 self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
667 self.yvtickpoint = self.axes["y"].vtickpoint
668 self.ytickdirection = self.axes["y"].tickdirection
669 self.yvtickdirection = self.axes["y"].vtickdirection
671 if self.axes.has_key("z"):
672 self.zbasepath = self.axes["z"].basepath
673 self.zvbasepath = self.axes["z"].vbasepath
674 self.zgridpath = self.axes["z"].gridpath
675 self.ztickpoint_pt = self.axes["z"].tickpoint_pt
676 self.ztickpoint = self.axes["z"].tickpoint
677 self.zvtickpoint_pt = self.axes["z"].vtickpoint
678 self.zvtickpoint = self.axes["z"].vtickpoint
679 self.ztickdirection = self.axes["z"].tickdirection
680 self.zvtickdirection = self.axes["z"].vtickdirection
682 self.axesnames = ([], [], [])
683 for axisname, aaxis in self.axes.items():
684 if axisname[0] not in "xyz" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
685 axisname[1:] == "1")):
686 raise ValueError("invalid axis name")
687 if axisname[0] == "x":
688 self.axesnames[0].append(axisname)
689 elif axisname[0] == "y":
690 self.axesnames[1].append(axisname)
691 else:
692 self.axesnames[2].append(axisname)
693 aaxis.setcreatecall(self.doaxiscreate, axisname)
695 def pos_pt(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
696 if xaxis is None:
697 xaxis = self.axes["x"]
698 if yaxis is None:
699 yaxis = self.axes["y"]
700 if zaxis is None:
701 zaxis = self.axes["z"]
702 return self.vpos_pt(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
704 def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
705 if xaxis is None:
706 xaxis = self.axes["x"]
707 if yaxis is None:
708 yaxis = self.axes["y"]
709 if zaxis is None:
710 zaxis = self.axes["z"]
711 return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
713 def vpos_pt(self, vx, vy, vz):
714 x, y = self.projector.point(2*self.xscale*(vx - 0.5),
715 2*self.yscale*(vy - 0.5),
716 2*self.zscale*(vz - 0.5))
717 return self.xpos_pt+x*self.size_pt, self.ypos_pt+y*self.size_pt
719 def vpos(self, vx, vy, vz):
720 x, y = self.projector.point(2*self.xscale*(vx - 0.5),
721 2*self.yscale*(vy - 0.5),
722 2*self.zscale*(vz - 0.5))
723 return self.xpos+x*self.size, self.ypos+y*self.size
725 def vzindex(self, vx, vy, vz):
726 return self.projector.zindex(2*self.xscale*(vx - 0.5),
727 2*self.yscale*(vy - 0.5),
728 2*self.zscale*(vz - 0.5))/self.zindexscale
730 def vangle(self, vx1, vy1, vz1, vx2, vy2, vz2, vx3, vy3, vz3):
731 return self.projector.angle(2*self.xscale*(vx1 - 0.5),
732 2*self.yscale*(vy1 - 0.5),
733 2*self.zscale*(vz1 - 0.5),
734 2*self.xscale*(vx2 - 0.5),
735 2*self.yscale*(vy2 - 0.5),
736 2*self.zscale*(vz2 - 0.5),
737 2*self.xscale*(vx3 - 0.5),
738 2*self.yscale*(vy3 - 0.5),
739 2*self.zscale*(vz3 - 0.5))
741 def vgeodesic(self, vx1, vy1, vz1, vx2, vy2, vz2):
742 """returns a geodesic path between two points in graph coordinates"""
743 return path.line_pt(*(self.vpos_pt(vx1, vy1, vz1) + self.vpos_pt(vx2, vy2, vz2)))
745 def vgeodesic_el(self, vx1, vy1, vz1, vx2, vy2, vz2):
746 """returns a geodesic path element between two points in graph coordinates"""
747 return path.lineto_pt(*self.vpos_pt(vx2, vy2, vz2))
749 def vcap_pt(self, coordinate, length_pt, vx, vy, vz):
750 """returns an error cap path for a given coordinate, lengths and
751 point in graph coordinates"""
752 if coordinate == 0:
753 return self.vgeodesic(vx-0.5*length_pt/self.size_pt, vy, vz, vx+0.5*length_pt/self.size_pt, vy, vz)
754 elif coordinate == 1:
755 return self.vgeodesic(vx, vy-0.5*length_pt/self.size_pt, vz, vx, vy+0.5*length_pt/self.size_pt, vz)
756 elif coordinate == 2:
757 return self.vgeodesic(vx, vy, vz-0.5*length_pt/self.size_pt, vx, vy, vz+0.5*length_pt/self.size_pt)
758 else:
759 raise ValueError("direction invalid")
761 def xvtickdirection(self, vx):
762 if self.xorder:
763 x1_pt, y1_pt = self.vpos_pt(vx, 1, 0)
764 x2_pt, y2_pt = self.vpos_pt(vx, 0, 0)
765 else:
766 x1_pt, y1_pt = self.vpos_pt(vx, 0, 0)
767 x2_pt, y2_pt = self.vpos_pt(vx, 1, 0)
768 dx_pt = x2_pt - x1_pt
769 dy_pt = y2_pt - y1_pt
770 norm = math.hypot(dx_pt, dy_pt)
771 return dx_pt/norm, dy_pt/norm
773 def yvtickdirection(self, vy):
774 if self.yorder:
775 x1_pt, y1_pt = self.vpos_pt(1, vy, 0)
776 x2_pt, y2_pt = self.vpos_pt(0, vy, 0)
777 else:
778 x1_pt, y1_pt = self.vpos_pt(0, vy, 0)
779 x2_pt, y2_pt = self.vpos_pt(1, vy, 0)
780 dx_pt = x2_pt - x1_pt
781 dy_pt = y2_pt - y1_pt
782 norm = math.hypot(dx_pt, dy_pt)
783 return dx_pt/norm, dy_pt/norm
785 def vtickdirection(self, vx1, vy1, vz1, vx2, vy2, vz2):
786 x1_pt, y1_pt = self.vpos_pt(vx1, vy1, vz1)
787 x2_pt, y2_pt = self.vpos_pt(vx2, vy2, vz2)
788 dx_pt = x2_pt - x1_pt
789 dy_pt = y2_pt - y1_pt
790 norm = math.hypot(dx_pt, dy_pt)
791 return dx_pt/norm, dy_pt/norm
793 def xvgridpath(self, vx):
794 return path.path(path.moveto_pt(*self.vpos_pt(vx, 0, 0)),
795 path.lineto_pt(*self.vpos_pt(vx, 1, 0)),
796 path.lineto_pt(*self.vpos_pt(vx, 1, 1)),
797 path.lineto_pt(*self.vpos_pt(vx, 0, 1)),
798 path.closepath())
800 def yvgridpath(self, vy):
801 return path.path(path.moveto_pt(*self.vpos_pt(0, vy, 0)),
802 path.lineto_pt(*self.vpos_pt(1, vy, 0)),
803 path.lineto_pt(*self.vpos_pt(1, vy, 1)),
804 path.lineto_pt(*self.vpos_pt(0, vy, 1)),
805 path.closepath())
807 def zvgridpath(self, vz):
808 return path.path(path.moveto_pt(*self.vpos_pt(0, 0, vz)),
809 path.lineto_pt(*self.vpos_pt(1, 0, vz)),
810 path.lineto_pt(*self.vpos_pt(1, 1, vz)),
811 path.lineto_pt(*self.vpos_pt(0, 1, vz)),
812 path.closepath())
814 def doaxispositioner(self, axisname):
815 if self.did(self.doaxispositioner, axisname):
816 return
817 self.doranges()
818 if axisname == "x":
819 self.axes["x"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 0),
820 lambda vx: self.vtickdirection(vx, self.xorder, 0, vx, 1-self.xorder, 0),
821 self.xvgridpath))
822 elif axisname == "x2":
823 self.axes["x2"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 0),
824 lambda vx: self.vtickdirection(vx, 1-self.xorder, 0, vx, self.xorder, 0),
825 self.xvgridpath))
826 elif axisname == "x3":
827 self.axes["x3"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 1),
828 lambda vx: self.vtickdirection(vx, self.xorder, 1, vx, 1-self.xorder, 1),
829 self.xvgridpath))
830 elif axisname == "x4":
831 self.axes["x4"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 1),
832 lambda vx: self.vtickdirection(vx, 1-self.xorder, 1, vx, self.xorder, 1),
833 self.xvgridpath))
834 elif axisname == "y":
835 self.axes["y"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 0),
836 lambda vy: self.vtickdirection(self.yorder, vy, 0, 1-self.yorder, vy, 0),
837 self.yvgridpath))
838 elif axisname == "y2":
839 self.axes["y2"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 0),
840 lambda vy: self.vtickdirection(1-self.yorder, vy, 0, self.yorder, vy, 0),
841 self.yvgridpath))
842 elif axisname == "y3":
843 self.axes["y3"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 1),
844 lambda vy: self.vtickdirection(self.yorder, vy, 1, 1-self.yorder, vy, 1),
845 self.yvgridpath))
846 elif axisname == "y4":
847 self.axes["y4"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 1),
848 lambda vy: self.vtickdirection(1-self.yorder, vy, 1, self.yorder, vy, 1),
849 self.yvgridpath))
850 elif axisname == "z":
851 self.axes["z"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 0, vz),
852 lambda vz: self.vtickdirection(0, 0, vz, 1, 1, vz),
853 self.zvgridpath))
854 elif axisname == "z2":
855 self.axes["z2"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 0, vz),
856 lambda vz: self.vtickdirection(1, 0, vz, 0, 1, vz),
857 self.zvgridpath))
858 elif axisname == "z3":
859 self.axes["z3"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 1, vz),
860 lambda vz: self.vtickdirection(0, 1, vz, 1, 0, vz),
861 self.zvgridpath))
862 elif axisname == "z4":
863 self.axes["z4"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 1, vz),
864 lambda vz: self.vtickdirection(1, 1, vz, 0, 0, vz),
865 self.zvgridpath))
866 else:
867 raise NotImplementedError("4 axis per dimension supported only")
869 def dolayout(self):
870 if self.did(self.dolayout):
871 return
872 for axisname in self.axes.keys():
873 self.doaxiscreate(axisname)
875 def dobackground(self):
876 if self.did(self.dobackground):
877 return
879 def doaxes(self):
880 if self.did(self.doaxes):
881 return
882 self.dolayout()
883 self.dobackground()
884 for axis in self.axes.values():
885 self.insert(axis.canvas)
887 def dokey(self):
888 if self.did(self.dokey):
889 return
890 self.dobackground()
891 for plotitem in self.plotitems:
892 self.dokeyitem(plotitem)
893 if self.key is not None:
894 c = self.key.paint(self.keyitems)
895 bbox = c.bbox()
896 def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
897 ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
898 cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
899 return ppos-cpos
900 if bbox:
901 x = parentchildalign(self.xpos_pt, self.xpos_pt+self.size_pt,
902 bbox.llx_pt, bbox.urx_pt,
903 self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
904 y = parentchildalign(self.ypos_pt, self.ypos_pt+self.size_pt,
905 bbox.lly_pt, bbox.ury_pt,
906 self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
907 self.insert(c, [trafo.translate_pt(x, y)])