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