add a one dimensional graph
[PyX.git] / pyx / graph / graph.py
blob4ab7099c02f42c513d414e1aa34e889344ea449a
1 # -*- encoding: utf-8 -*-
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-2011 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, pycompat, trafo, unit
27 from pyx.graph.axis import axis, positioner
30 goldenmean = 0.5 * (math.sqrt(5) + 1)
33 # The following two methods are used to register and get a default provider
34 # for keys. A key is a variable name in sharedata. A provider is a style
35 # which creates variables in sharedata.
37 _defaultprovider = {}
39 def registerdefaultprovider(style, keys):
40 """sets a style as a default creator for sharedata variables 'keys'"""
41 for key in keys:
42 assert key in style.providesdata, "key not provided by style"
43 # we might allow for overwriting the defaults, i.e. the following is not checked:
44 # assert key in _defaultprovider.keys(), "default provider already registered for key"
45 _defaultprovider[key] = style
47 def getdefaultprovider(key):
48 """returns a style, which acts as a default creator for the
49 sharedata variable 'key'"""
50 return _defaultprovider[key]
53 class styledata:
54 """style data storage class
56 Instances of this class are used to store data from the styles
57 and to pass point data to the styles by instances named privatedata
58 and sharedata. sharedata is shared between all the style(s) in use
59 by a data instance, while privatedata is private to each style and
60 used as a storage place instead of self to prevent side effects when
61 using a style several times."""
62 pass
65 class plotitem:
67 def __init__(self, graph, data, styles):
68 self.data = data
69 self.title = data.title
71 addstyles = [None]
72 while addstyles:
73 # add styles to ensure all needs of the given styles
74 provided = [] # already provided sharedata variables
75 addstyles = [] # a list of style instances to be added in front
76 for s in styles:
77 for n in s.needsdata:
78 if n not in provided:
79 defaultprovider = getdefaultprovider(n)
80 addstyles.append(defaultprovider)
81 provided.extend(defaultprovider.providesdata)
82 provided.extend(s.providesdata)
83 styles = addstyles + styles
85 self.styles = styles
86 self.sharedata = styledata()
87 self.dataaxisnames = {}
88 self.privatedatalist = [styledata() for s in self.styles]
90 # perform setcolumns to all styles
91 self.usedcolumnnames = pycompat.set()
92 for privatedata, s in zip(self.privatedatalist, self.styles):
93 self.usedcolumnnames.update(pycompat.set(s.columnnames(privatedata, self.sharedata, graph, self.data.columnnames, self.dataaxisnames)))
95 def selectstyles(self, graph, selectindex, selecttotal):
96 for privatedata, style in zip(self.privatedatalist, self.styles):
97 style.selectstyle(privatedata, self.sharedata, graph, selectindex, selecttotal)
99 def adjustaxesstatic(self, graph):
100 for columnname, data in self.data.columns.items():
101 for privatedata, style in zip(self.privatedatalist, self.styles):
102 style.adjustaxis(privatedata, self.sharedata, graph, columnname, data)
104 def makedynamicdata(self, graph):
105 self.dynamiccolumns = self.data.dynamiccolumns(graph, self.dataaxisnames)
107 def adjustaxesdynamic(self, graph):
108 for columnname, data in self.dynamiccolumns.items():
109 for privatedata, style in zip(self.privatedatalist, self.styles):
110 style.adjustaxis(privatedata, self.sharedata, graph, columnname, data)
112 def draw(self, graph):
113 for privatedata, style in zip(self.privatedatalist, self.styles):
114 style.initdrawpoints(privatedata, self.sharedata, graph)
115 point = {}
116 useitems = []
117 for columnname in self.usedcolumnnames:
118 try:
119 useitems.append((columnname, self.dynamiccolumns[columnname]))
120 except KeyError:
121 useitems.append((columnname, self.data.columns[columnname]))
122 if not useitems:
123 raise ValueError("cannot draw empty data")
124 for i in xrange(len(useitems[0][1])):
125 for columnname, data in useitems:
126 point[columnname] = data[i]
127 for privatedata, style in zip(self.privatedatalist, self.styles):
128 style.drawpoint(privatedata, self.sharedata, graph, point)
129 for privatedata, style in zip(self.privatedatalist, self.styles):
130 style.donedrawpoints(privatedata, self.sharedata, graph)
132 def key_pt(self, graph, x_pt, y_pt, width_pt, height_pt):
133 for privatedata, style in zip(self.privatedatalist, self.styles):
134 style.key_pt(privatedata, self.sharedata, graph, x_pt, y_pt, width_pt, height_pt)
136 def __getattr__(self, attr):
137 # read only access to the styles privatedata
138 # this is just a convenience method
139 # use case: access the path of a the line style
140 stylesdata = [getattr(styledata, attr)
141 for styledata in self.privatedatalist
142 if hasattr(styledata, attr)]
143 if len(stylesdata) > 1:
144 return stylesdata
145 elif len(stylesdata) == 1:
146 return stylesdata[0]
147 raise AttributeError("access to styledata attribute '%s' failed" % attr)
150 class graph(canvas.canvas):
152 def __init__(self):
153 canvas.canvas.__init__(self)
154 for name in ["background", "filldata", "axes.grid", "axis.baseline", "axis.ticks", "axis.labels", "axis.title", "data", "key"]:
155 self.layer(name)
156 self.axes = {}
157 self.plotitems = []
158 self.keyitems = []
159 self._calls = {}
160 self.didranges = 0
161 self.didstyles = 0
163 def did(self, method, *args, **kwargs):
164 if not self._calls.has_key(method):
165 self._calls[method] = []
166 for callargs in self._calls[method]:
167 if callargs == (args, kwargs):
168 return 1
169 self._calls[method].append((args, kwargs))
170 return 0
172 def bbox(self):
173 self.finish()
174 return canvas.canvas.bbox(self)
177 def registerPS(self, registry):
178 self.finish()
179 canvas.canvas.registerPS(self, registry)
181 def registerPDF(self, registry):
182 self.finish()
183 canvas.canvas.registerPDF(self, registry)
185 def processPS(self, file, writer, context, registry, bbox):
186 self.finish()
187 canvas.canvas.processPS(self, file, writer, context, registry, bbox)
189 def processPDF(self, file, writer, context, registry, bbox):
190 self.finish()
191 canvas.canvas.processPDF(self, file, writer, context, registry, bbox)
193 def plot(self, data, styles=None, rangewarning=1):
194 if self.didranges and rangewarning:
195 warnings.warn("axes ranges have already been analysed; no further adjustments will be performed")
196 if self.didstyles:
197 raise RuntimeError("can't plot further data after dostyles() has been executed")
198 singledata = 0
199 try:
200 for d in data:
201 pass
202 except:
203 usedata = [data]
204 singledata = 1
205 else:
206 usedata = data
207 if styles is None:
208 for d in usedata:
209 if styles is None:
210 styles = d.defaultstyles
211 elif styles != d.defaultstyles:
212 raise RuntimeError("defaultstyles differ")
213 plotitems = []
214 for d in usedata:
215 plotitems.append(plotitem(self, d, styles))
216 self.plotitems.extend(plotitems)
217 if self.didranges:
218 for aplotitem in plotitems:
219 aplotitem.makedynamicdata(self)
220 if singledata:
221 return plotitems[0]
222 else:
223 return plotitems
225 def doranges(self):
226 if self.did(self.doranges):
227 return
228 for plotitem in self.plotitems:
229 plotitem.adjustaxesstatic(self)
230 for plotitem in self.plotitems:
231 plotitem.makedynamicdata(self)
232 for plotitem in self.plotitems:
233 plotitem.adjustaxesdynamic(self)
234 self.didranges = 1
236 def doaxiscreate(self, axisname):
237 if self.did(self.doaxiscreate, axisname):
238 return
239 self.doaxispositioner(axisname)
240 self.axes[axisname].create()
242 def dolayout(self):
243 raise NotImplementedError
245 def dobackground(self):
246 pass
248 def doaxes(self):
249 raise NotImplementedError
251 def dostyles(self):
252 if self.did(self.dostyles):
253 return
254 self.dolayout()
255 self.dobackground()
257 # count the usage of styles and perform selects
258 styletotal = {}
259 def stylesid(styles):
260 return ":".join([str(id(style)) for style in styles])
261 for plotitem in self.plotitems:
262 try:
263 styletotal[stylesid(plotitem.styles)] += 1
264 except:
265 styletotal[stylesid(plotitem.styles)] = 1
266 styleindex = {}
267 for plotitem in self.plotitems:
268 try:
269 styleindex[stylesid(plotitem.styles)] += 1
270 except:
271 styleindex[stylesid(plotitem.styles)] = 0
272 plotitem.selectstyles(self, styleindex[stylesid(plotitem.styles)],
273 styletotal[stylesid(plotitem.styles)])
275 self.didstyles = 1
277 def doplotitem(self, plotitem):
278 if self.did(self.doplotitem, plotitem):
279 return
280 self.dostyles()
281 plotitem.draw(self)
283 def doplot(self):
284 for plotitem in self.plotitems:
285 self.doplotitem(plotitem)
287 def dodata(self):
288 warnings.warn("dodata() has been deprecated. Use doplot() instead.")
289 self.doplot()
291 def dokeyitem(self, plotitem):
292 if self.did(self.dokeyitem, plotitem):
293 return
294 self.dostyles()
295 if plotitem.title is not None:
296 self.keyitems.append(plotitem)
298 def dokey(self):
299 raise NotImplementedError
301 def finish(self):
302 self.dobackground()
303 self.doaxes()
304 self.doplot()
305 self.dokey()
308 class graphxy(graph):
310 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
311 key=None, backgroundattrs=None, axesdist=0.8*unit.v_cm, flipped=False,
312 xaxisat=None, yaxisat=None, **axes):
313 graph.__init__(self)
315 self.xpos = xpos
316 self.ypos = ypos
317 self.xpos_pt = unit.topt(self.xpos)
318 self.ypos_pt = unit.topt(self.ypos)
319 self.xaxisat = xaxisat
320 self.yaxisat = yaxisat
321 self.key = key
322 self.backgroundattrs = backgroundattrs
323 self.axesdist_pt = unit.topt(axesdist)
324 self.flipped = flipped
326 self.width = width
327 self.height = height
328 if width is None:
329 if height is None:
330 raise ValueError("specify width and/or height")
331 else:
332 self.width = ratio * self.height
333 elif height is None:
334 self.height = (1.0/ratio) * self.width
335 self.width_pt = unit.topt(self.width)
336 self.height_pt = unit.topt(self.height)
338 for axisname, aaxis in axes.items():
339 if aaxis is not None:
340 if not isinstance(aaxis, axis.linkedaxis):
341 self.axes[axisname] = axis.anchoredaxis(aaxis, self.texrunner, axisname)
342 else:
343 self.axes[axisname] = aaxis
344 for axisname, axisat in [("x", xaxisat), ("y", yaxisat)]:
345 okey = axisname + "2"
346 if not axes.has_key(axisname):
347 if not axes.has_key(okey) or axes[okey] is None:
348 self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.texrunner, axisname)
349 if not axes.has_key(okey):
350 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
351 else:
352 self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
353 elif not axes.has_key(okey) and axisat is None:
354 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
356 if self.axes.has_key("x"):
357 self.xbasepath = self.axes["x"].basepath
358 self.xvbasepath = self.axes["x"].vbasepath
359 self.xgridpath = self.axes["x"].gridpath
360 self.xtickpoint_pt = self.axes["x"].tickpoint_pt
361 self.xtickpoint = self.axes["x"].tickpoint
362 self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
363 self.xvtickpoint = self.axes["x"].tickpoint
364 self.xtickdirection = self.axes["x"].tickdirection
365 self.xvtickdirection = self.axes["x"].vtickdirection
367 if self.axes.has_key("y"):
368 self.ybasepath = self.axes["y"].basepath
369 self.yvbasepath = self.axes["y"].vbasepath
370 self.ygridpath = self.axes["y"].gridpath
371 self.ytickpoint_pt = self.axes["y"].tickpoint_pt
372 self.ytickpoint = self.axes["y"].tickpoint
373 self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
374 self.yvtickpoint = self.axes["y"].vtickpoint
375 self.ytickdirection = self.axes["y"].tickdirection
376 self.yvtickdirection = self.axes["y"].vtickdirection
378 self.axesnames = ([], [])
379 for axisname, aaxis in self.axes.items():
380 if axisname[0] not in "xy" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
381 axisname[1:] == "1")):
382 raise ValueError("invalid axis name")
383 if axisname[0] == "x":
384 self.axesnames[0].append(axisname)
385 else:
386 self.axesnames[1].append(axisname)
387 aaxis.setcreatecall(self.doaxiscreate, axisname)
389 self.axespositioners = dict(x=positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
390 self.xpos_pt + self.width_pt, self.ypos_pt,
391 (0, 1), self.xvgridpath),
392 x2=positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt + self.height_pt,
393 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
394 (0, -1), self.xvgridpath),
395 y=positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt,
396 self.xpos_pt, self.ypos_pt + self.height_pt,
397 (1, 0), self.yvgridpath),
398 y2=positioner.lineaxispos_pt(self.xpos_pt + self.width_pt, self.ypos_pt,
399 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt,
400 (-1, 0), self.yvgridpath))
401 if self.flipped:
402 self.axespositioners = dict(x=self.axespositioners["y2"],
403 y2=self.axespositioners["x2"],
404 y=self.axespositioners["x"],
405 x2=self.axespositioners["y"])
407 def pos_pt(self, x, y, xaxis=None, yaxis=None):
408 if xaxis is None:
409 xaxis = self.axes["x"]
410 if yaxis is None:
411 yaxis = self.axes["y"]
412 vx = xaxis.convert(x)
413 vy = yaxis.convert(y)
414 if self.flipped:
415 vx, vy = vy, vx
416 return (self.xpos_pt + vx*self.width_pt,
417 self.ypos_pt + vy*self.height_pt)
419 def pos(self, x, y, xaxis=None, yaxis=None):
420 if xaxis is None:
421 xaxis = self.axes["x"]
422 if yaxis is None:
423 yaxis = self.axes["y"]
424 vx = xaxis.convert(x)
425 vy = yaxis.convert(y)
426 if self.flipped:
427 vx, vy = vy, vx
428 return (self.xpos + vx*self.width,
429 self.ypos + vy*self.height)
431 def vpos_pt(self, vx, vy):
432 if self.flipped:
433 vx, vy = vy, vx
434 return (self.xpos_pt + vx*self.width_pt,
435 self.ypos_pt + vy*self.height_pt)
437 def vpos(self, vx, vy):
438 if self.flipped:
439 vx, vy = vy, vx
440 return (self.xpos + vx*self.width,
441 self.ypos + vy*self.height)
443 def vzindex(self, vx, vy):
444 return 0
446 def vangle(self, vx1, vy1, vx2, vy2, vx3, vy3):
447 return 1
449 def vgeodesic(self, vx1, vy1, vx2, vy2):
450 """returns a geodesic path between two points in graph coordinates"""
451 if self.flipped:
452 vx1, vy1 = vy1, vx1
453 vx2, vy2 = vy2, vx2
454 return path.line_pt(self.xpos_pt + vx1*self.width_pt,
455 self.ypos_pt + vy1*self.height_pt,
456 self.xpos_pt + vx2*self.width_pt,
457 self.ypos_pt + vy2*self.height_pt)
459 def vgeodesic_el(self, vx1, vy1, vx2, vy2):
460 """returns a geodesic path element between two points in graph coordinates"""
461 if self.flipped:
462 vx1, vy1 = vy1, vx1
463 vx2, vy2 = vy2, vx2
464 return path.lineto_pt(self.xpos_pt + vx2*self.width_pt,
465 self.ypos_pt + vy2*self.height_pt)
467 def vcap_pt(self, coordinate, length_pt, vx, vy):
468 """returns an error cap path for a given coordinate, lengths and
469 point in graph coordinates"""
470 if self.flipped:
471 coordinate = 1-coordinate
472 vx, vy = vy, vx
473 if coordinate == 0:
474 return path.line_pt(self.xpos_pt + vx*self.width_pt - 0.5*length_pt,
475 self.ypos_pt + vy*self.height_pt,
476 self.xpos_pt + vx*self.width_pt + 0.5*length_pt,
477 self.ypos_pt + vy*self.height_pt)
478 elif coordinate == 1:
479 return path.line_pt(self.xpos_pt + vx*self.width_pt,
480 self.ypos_pt + vy*self.height_pt - 0.5*length_pt,
481 self.xpos_pt + vx*self.width_pt,
482 self.ypos_pt + vy*self.height_pt + 0.5*length_pt)
483 else:
484 raise ValueError("direction invalid")
486 def xvgridpath(self, vx):
487 return path.line_pt(self.xpos_pt + vx*self.width_pt, self.ypos_pt,
488 self.xpos_pt + vx*self.width_pt, self.ypos_pt + self.height_pt)
490 def yvgridpath(self, vy):
491 return path.line_pt(self.xpos_pt, self.ypos_pt + vy*self.height_pt,
492 self.xpos_pt + self.width_pt, self.ypos_pt + vy*self.height_pt)
494 def axisatv(self, axis, v):
495 if axis.positioner.fixtickdirection[0]:
496 # it is a y-axis
497 t = trafo.translate_pt(self.xpos_pt + v*self.width_pt - axis.positioner.x1_pt, 0)
498 else:
499 # it is an x-axis
500 t = trafo.translate_pt(0, self.ypos_pt + v*self.height_pt - axis.positioner.y1_pt)
501 c = canvas.canvas()
502 for layer, subcanvas in axis.canvas.layers.items():
503 c.layer(layer).insert(subcanvas, [t])
504 assert len(axis.canvas.layers) == len(axis.canvas.items), str(axis.canvas.items)
505 axis.canvas = c
507 def doaxispositioner(self, axisname):
508 if self.did(self.doaxispositioner, axisname):
509 return
510 self.doranges()
511 if axisname in ["x", "x2", "y", "y2"]:
512 self.axes[axisname].setpositioner(self.axespositioners[axisname])
513 else:
514 if axisname[1:] == "3":
515 dependsonaxisname = axisname[0]
516 else:
517 dependsonaxisname = "%s%d" % (axisname[0], int(axisname[1:]) - 2)
518 self.doaxiscreate(dependsonaxisname)
519 sign = 2*(int(axisname[1:]) % 2) - 1
520 if axisname[0] == "x" and self.flipped:
521 sign = -sign
522 if axisname[0] == "x" and not self.flipped or axisname[0] == "y" and self.flipped:
523 y_pt = self.axes[dependsonaxisname].positioner.y1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
524 self.axes[axisname].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, y_pt,
525 self.xpos_pt + self.width_pt, y_pt,
526 (0, sign), self.xvgridpath))
527 else:
528 x_pt = self.axes[dependsonaxisname].positioner.x1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt)
529 self.axes[axisname].setpositioner(positioner.lineaxispos_pt(x_pt, self.ypos_pt,
530 x_pt, self.ypos_pt + self.height_pt,
531 (sign, 0), self.yvgridpath))
533 def dolayout(self):
534 if self.did(self.dolayout):
535 return
536 for axisname in self.axes.keys():
537 self.doaxiscreate(axisname)
538 if self.xaxisat is not None:
539 self.axisatv(self.axes["x"], self.axes["y"].convert(self.xaxisat))
540 if self.yaxisat is not None:
541 self.axisatv(self.axes["y"], self.axes["x"].convert(self.yaxisat))
543 def dobackground(self):
544 if self.did(self.dobackground):
545 return
546 if self.backgroundattrs is not None:
547 self.layer("background").draw(path.rect_pt(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt),
548 self.backgroundattrs)
550 def doaxes(self):
551 if self.did(self.doaxes):
552 return
553 self.dolayout()
554 self.dobackground()
555 for axis in self.axes.values():
556 for layer, canvas in axis.canvas.layers.items():
557 self.layer("axes.%s" % layer).insert(canvas)
558 assert len(axis.canvas.layers) == len(axis.canvas.items), str(axis.canvas.items)
560 def dokey(self):
561 if self.did(self.dokey):
562 return
563 self.dobackground()
564 for plotitem in self.plotitems:
565 self.dokeyitem(plotitem)
566 if self.key is not None:
567 c = self.key.paint(self.keyitems)
568 bbox = c.bbox()
569 def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
570 ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
571 cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
572 return ppos-cpos
573 if bbox:
574 x = parentchildalign(self.xpos_pt, self.xpos_pt+self.width_pt,
575 bbox.llx_pt, bbox.urx_pt,
576 self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
577 y = parentchildalign(self.ypos_pt, self.ypos_pt+self.height_pt,
578 bbox.lly_pt, bbox.ury_pt,
579 self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
580 self.layer("key").insert(c, [trafo.translate_pt(x, y)])
584 class graphx(graphxy):
586 def __init__(self, xpos=0, ypos=0, length=None, size=0.5*unit.v_cm, direction="vertical",
587 key=None, backgroundattrs=None, axesdist=0.8*unit.v_cm, **axes):
588 for name in axes:
589 if not name.startswith("x"):
590 raise ValueError("Only x axes are allowed")
591 self.direction = direction
592 if self.direction == "vertical":
593 kwargsxy = dict(width=size, height=length, flipped=True)
594 elif self.direction == "horizontal":
595 kwargsxy = dict(width=length, height=size)
596 else:
597 raise ValueError("vertical or horizontal direction required")
598 kwargsxy.update(**axes)
600 graphxy.__init__(self, xpos=xpos, ypos=ypos, ratio=None, key=key, y=axis.lin(min=0, max=1, parter=None),
601 backgroundattrs=backgroundattrs, axesdist=axesdist, **kwargsxy)
603 def pos_pt(self, x, xaxis=None):
604 return graphxy.pos_pt(self, x, 0.5, xaxis)
606 def pos(self, x, xaxis=None):
607 return graphxy.pos(self, x, 0.5, xaxis)
609 def vpos_pt(self, vx):
610 return graphxy.vpos_pt(self, vx, 0.5)
612 def vpos(self, vx):
613 return graphxy.vpos(self, vx, 0.5)
615 def vgeodesic(self, vx1, vx2):
616 return graphxy.vgeodesic(self, vx1, 0.5, vx2, 0.5)
618 def vgeodesic_el(self, vx1, vy1, vx2, vy2):
619 return graphxy.vgeodesic_el(self, vx1, 0.5, vx2, 0.5)
621 def vcap_pt(self, coordinate, length_pt, vx):
622 if coordinate == 0:
623 return graphxy.vcap_pt(self, coordinate, length_pt, vx, 0.5)
624 else:
625 raise ValueError("direction invalid")
627 def xvgridpath(self, vx):
628 return graphxy.xvgridpath(self, vx)
630 def yvgridpath(self, vy):
631 raise Exception("This method does not exist on a one dimensional graph.")
633 def axisatv(self, axis, v):
634 raise Exception("This method does not exist on a one dimensional graph.")
638 class graphxyz(graphxy):
640 class central:
642 def __init__(self, distance, phi, theta, anglefactor=math.pi/180):
643 phi *= anglefactor
644 theta *= anglefactor
645 self.distance = distance
647 self.a = (-math.sin(phi), math.cos(phi), 0)
648 self.b = (-math.cos(phi)*math.sin(theta),
649 -math.sin(phi)*math.sin(theta),
650 math.cos(theta))
651 self.eye = (distance*math.cos(phi)*math.cos(theta),
652 distance*math.sin(phi)*math.cos(theta),
653 distance*math.sin(theta))
655 def point(self, x, y, z):
656 d0 = (self.a[0]*self.b[1]*(z-self.eye[2])
657 + self.a[2]*self.b[0]*(y-self.eye[1])
658 + self.a[1]*self.b[2]*(x-self.eye[0])
659 - self.a[2]*self.b[1]*(x-self.eye[0])
660 - self.a[0]*self.b[2]*(y-self.eye[1])
661 - self.a[1]*self.b[0]*(z-self.eye[2]))
662 da = (self.eye[0]*self.b[1]*(z-self.eye[2])
663 + self.eye[2]*self.b[0]*(y-self.eye[1])
664 + self.eye[1]*self.b[2]*(x-self.eye[0])
665 - self.eye[2]*self.b[1]*(x-self.eye[0])
666 - self.eye[0]*self.b[2]*(y-self.eye[1])
667 - self.eye[1]*self.b[0]*(z-self.eye[2]))
668 db = (self.a[0]*self.eye[1]*(z-self.eye[2])
669 + self.a[2]*self.eye[0]*(y-self.eye[1])
670 + self.a[1]*self.eye[2]*(x-self.eye[0])
671 - self.a[2]*self.eye[1]*(x-self.eye[0])
672 - self.a[0]*self.eye[2]*(y-self.eye[1])
673 - self.a[1]*self.eye[0]*(z-self.eye[2]))
674 return da/d0, db/d0
676 def zindex(self, x, y, z):
677 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
679 def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
680 sx = (x1-self.eye[0])
681 sy = (y1-self.eye[1])
682 sz = (z1-self.eye[2])
683 nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
684 ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
685 nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
686 return (sx*nx+sy*ny+sz*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)/math.sqrt(sx*sx+sy*sy+sz*sz)
689 class parallel:
691 def __init__(self, phi, theta, anglefactor=math.pi/180):
692 phi *= anglefactor
693 theta *= anglefactor
695 self.a = (-math.sin(phi), math.cos(phi), 0)
696 self.b = (-math.cos(phi)*math.sin(theta),
697 -math.sin(phi)*math.sin(theta),
698 math.cos(theta))
699 self.c = (-math.cos(phi)*math.cos(theta),
700 -math.sin(phi)*math.cos(theta),
701 -math.sin(theta))
703 def point(self, x, y, z):
704 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
706 def zindex(self, x, y, z):
707 return self.c[0]*x+self.c[1]*y+self.c[2]*z
709 def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3):
710 nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1)
711 ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1)
712 nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1)
713 return (self.c[0]*nx+self.c[1]*ny+self.c[2]*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)
716 def __init__(self, xpos=0, ypos=0, size=None,
717 xscale=1, yscale=1, zscale=1/goldenmean,
718 projector=central(10, -30, 30), key=None,
719 **axes):
720 graph.__init__(self)
722 self.xpos = xpos
723 self.ypos = ypos
724 self.size = size
725 self.xpos_pt = unit.topt(xpos)
726 self.ypos_pt = unit.topt(ypos)
727 self.size_pt = unit.topt(size)
728 self.xscale = xscale
729 self.yscale = yscale
730 self.zscale = zscale
731 self.projector = projector
732 self.key = key
734 self.xorder = projector.zindex(0, -1, 0) > projector.zindex(0, 1, 0) and 1 or 0
735 self.yorder = projector.zindex(-1, 0, 0) > projector.zindex(1, 0, 0) and 1 or 0
736 self.zindexscale = math.sqrt(xscale*xscale+yscale*yscale+zscale*zscale)
738 for axisname, aaxis in axes.items():
739 if aaxis is not None:
740 if not isinstance(aaxis, axis.linkedaxis):
741 self.axes[axisname] = axis.anchoredaxis(aaxis, self.texrunner, axisname)
742 else:
743 self.axes[axisname] = aaxis
744 for axisname in ["x", "y"]:
745 okey = axisname + "2"
746 if not axes.has_key(axisname):
747 if not axes.has_key(okey) or axes[okey] is None:
748 self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.texrunner, axisname)
749 if not axes.has_key(okey):
750 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey)
751 else:
752 self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname)
753 if not axes.has_key("z"):
754 self.axes["z"] = axis.anchoredaxis(axis.linear(), self.texrunner, "z")
756 if self.axes.has_key("x"):
757 self.xbasepath = self.axes["x"].basepath
758 self.xvbasepath = self.axes["x"].vbasepath
759 self.xgridpath = self.axes["x"].gridpath
760 self.xtickpoint_pt = self.axes["x"].tickpoint_pt
761 self.xtickpoint = self.axes["x"].tickpoint
762 self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt
763 self.xvtickpoint = self.axes["x"].tickpoint
764 self.xtickdirection = self.axes["x"].tickdirection
765 self.xvtickdirection = self.axes["x"].vtickdirection
767 if self.axes.has_key("y"):
768 self.ybasepath = self.axes["y"].basepath
769 self.yvbasepath = self.axes["y"].vbasepath
770 self.ygridpath = self.axes["y"].gridpath
771 self.ytickpoint_pt = self.axes["y"].tickpoint_pt
772 self.ytickpoint = self.axes["y"].tickpoint
773 self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt
774 self.yvtickpoint = self.axes["y"].vtickpoint
775 self.ytickdirection = self.axes["y"].tickdirection
776 self.yvtickdirection = self.axes["y"].vtickdirection
778 if self.axes.has_key("z"):
779 self.zbasepath = self.axes["z"].basepath
780 self.zvbasepath = self.axes["z"].vbasepath
781 self.zgridpath = self.axes["z"].gridpath
782 self.ztickpoint_pt = self.axes["z"].tickpoint_pt
783 self.ztickpoint = self.axes["z"].tickpoint
784 self.zvtickpoint_pt = self.axes["z"].vtickpoint
785 self.zvtickpoint = self.axes["z"].vtickpoint
786 self.ztickdirection = self.axes["z"].tickdirection
787 self.zvtickdirection = self.axes["z"].vtickdirection
789 self.axesnames = ([], [], [])
790 for axisname, aaxis in self.axes.items():
791 if axisname[0] not in "xyz" or (len(axisname) != 1 and (not axisname[1:].isdigit() or
792 axisname[1:] == "1")):
793 raise ValueError("invalid axis name")
794 if axisname[0] == "x":
795 self.axesnames[0].append(axisname)
796 elif axisname[0] == "y":
797 self.axesnames[1].append(axisname)
798 else:
799 self.axesnames[2].append(axisname)
800 aaxis.setcreatecall(self.doaxiscreate, axisname)
802 def pos_pt(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
803 if xaxis is None:
804 xaxis = self.axes["x"]
805 if yaxis is None:
806 yaxis = self.axes["y"]
807 if zaxis is None:
808 zaxis = self.axes["z"]
809 return self.vpos_pt(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
811 def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
812 if xaxis is None:
813 xaxis = self.axes["x"]
814 if yaxis is None:
815 yaxis = self.axes["y"]
816 if zaxis is None:
817 zaxis = self.axes["z"]
818 return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
820 def vpos_pt(self, vx, vy, vz):
821 x, y = self.projector.point(2*self.xscale*(vx - 0.5),
822 2*self.yscale*(vy - 0.5),
823 2*self.zscale*(vz - 0.5))
824 return self.xpos_pt+x*self.size_pt, self.ypos_pt+y*self.size_pt
826 def vpos(self, vx, vy, vz):
827 x, y = self.projector.point(2*self.xscale*(vx - 0.5),
828 2*self.yscale*(vy - 0.5),
829 2*self.zscale*(vz - 0.5))
830 return self.xpos+x*self.size, self.ypos+y*self.size
832 def vzindex(self, vx, vy, vz):
833 return self.projector.zindex(2*self.xscale*(vx - 0.5),
834 2*self.yscale*(vy - 0.5),
835 2*self.zscale*(vz - 0.5))/self.zindexscale
837 def vangle(self, vx1, vy1, vz1, vx2, vy2, vz2, vx3, vy3, vz3):
838 return self.projector.angle(2*self.xscale*(vx1 - 0.5),
839 2*self.yscale*(vy1 - 0.5),
840 2*self.zscale*(vz1 - 0.5),
841 2*self.xscale*(vx2 - 0.5),
842 2*self.yscale*(vy2 - 0.5),
843 2*self.zscale*(vz2 - 0.5),
844 2*self.xscale*(vx3 - 0.5),
845 2*self.yscale*(vy3 - 0.5),
846 2*self.zscale*(vz3 - 0.5))
848 def vgeodesic(self, vx1, vy1, vz1, vx2, vy2, vz2):
849 """returns a geodesic path between two points in graph coordinates"""
850 return path.line_pt(*(self.vpos_pt(vx1, vy1, vz1) + self.vpos_pt(vx2, vy2, vz2)))
852 def vgeodesic_el(self, vx1, vy1, vz1, vx2, vy2, vz2):
853 """returns a geodesic path element between two points in graph coordinates"""
854 return path.lineto_pt(*self.vpos_pt(vx2, vy2, vz2))
856 def vcap_pt(self, coordinate, length_pt, vx, vy, vz):
857 """returns an error cap path for a given coordinate, lengths and
858 point in graph coordinates"""
859 if coordinate == 0:
860 return self.vgeodesic(vx-0.5*length_pt/self.size_pt, vy, vz, vx+0.5*length_pt/self.size_pt, vy, vz)
861 elif coordinate == 1:
862 return self.vgeodesic(vx, vy-0.5*length_pt/self.size_pt, vz, vx, vy+0.5*length_pt/self.size_pt, vz)
863 elif coordinate == 2:
864 return self.vgeodesic(vx, vy, vz-0.5*length_pt/self.size_pt, vx, vy, vz+0.5*length_pt/self.size_pt)
865 else:
866 raise ValueError("direction invalid")
868 def xvtickdirection(self, vx):
869 if self.xorder:
870 x1_pt, y1_pt = self.vpos_pt(vx, 1, 0)
871 x2_pt, y2_pt = self.vpos_pt(vx, 0, 0)
872 else:
873 x1_pt, y1_pt = self.vpos_pt(vx, 0, 0)
874 x2_pt, y2_pt = self.vpos_pt(vx, 1, 0)
875 dx_pt = x2_pt - x1_pt
876 dy_pt = y2_pt - y1_pt
877 norm = math.hypot(dx_pt, dy_pt)
878 return dx_pt/norm, dy_pt/norm
880 def yvtickdirection(self, vy):
881 if self.yorder:
882 x1_pt, y1_pt = self.vpos_pt(1, vy, 0)
883 x2_pt, y2_pt = self.vpos_pt(0, vy, 0)
884 else:
885 x1_pt, y1_pt = self.vpos_pt(0, vy, 0)
886 x2_pt, y2_pt = self.vpos_pt(1, vy, 0)
887 dx_pt = x2_pt - x1_pt
888 dy_pt = y2_pt - y1_pt
889 norm = math.hypot(dx_pt, dy_pt)
890 return dx_pt/norm, dy_pt/norm
892 def vtickdirection(self, vx1, vy1, vz1, vx2, vy2, vz2):
893 x1_pt, y1_pt = self.vpos_pt(vx1, vy1, vz1)
894 x2_pt, y2_pt = self.vpos_pt(vx2, vy2, vz2)
895 dx_pt = x2_pt - x1_pt
896 dy_pt = y2_pt - y1_pt
897 norm = math.hypot(dx_pt, dy_pt)
898 return dx_pt/norm, dy_pt/norm
900 def xvgridpath(self, vx):
901 return path.path(path.moveto_pt(*self.vpos_pt(vx, 0, 0)),
902 path.lineto_pt(*self.vpos_pt(vx, 1, 0)),
903 path.lineto_pt(*self.vpos_pt(vx, 1, 1)),
904 path.lineto_pt(*self.vpos_pt(vx, 0, 1)),
905 path.closepath())
907 def yvgridpath(self, vy):
908 return path.path(path.moveto_pt(*self.vpos_pt(0, vy, 0)),
909 path.lineto_pt(*self.vpos_pt(1, vy, 0)),
910 path.lineto_pt(*self.vpos_pt(1, vy, 1)),
911 path.lineto_pt(*self.vpos_pt(0, vy, 1)),
912 path.closepath())
914 def zvgridpath(self, vz):
915 return path.path(path.moveto_pt(*self.vpos_pt(0, 0, vz)),
916 path.lineto_pt(*self.vpos_pt(1, 0, vz)),
917 path.lineto_pt(*self.vpos_pt(1, 1, vz)),
918 path.lineto_pt(*self.vpos_pt(0, 1, vz)),
919 path.closepath())
921 def doaxispositioner(self, axisname):
922 if self.did(self.doaxispositioner, axisname):
923 return
924 self.doranges()
925 if axisname == "x":
926 self.axes["x"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 0),
927 lambda vx: self.vtickdirection(vx, self.xorder, 0, vx, 1-self.xorder, 0),
928 self.xvgridpath))
929 elif axisname == "x2":
930 self.axes["x2"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 0),
931 lambda vx: self.vtickdirection(vx, 1-self.xorder, 0, vx, self.xorder, 0),
932 self.xvgridpath))
933 elif axisname == "x3":
934 self.axes["x3"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 1),
935 lambda vx: self.vtickdirection(vx, self.xorder, 1, vx, 1-self.xorder, 1),
936 self.xvgridpath))
937 elif axisname == "x4":
938 self.axes["x4"].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 1),
939 lambda vx: self.vtickdirection(vx, 1-self.xorder, 1, vx, self.xorder, 1),
940 self.xvgridpath))
941 elif axisname == "y":
942 self.axes["y"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 0),
943 lambda vy: self.vtickdirection(self.yorder, vy, 0, 1-self.yorder, vy, 0),
944 self.yvgridpath))
945 elif axisname == "y2":
946 self.axes["y2"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 0),
947 lambda vy: self.vtickdirection(1-self.yorder, vy, 0, self.yorder, vy, 0),
948 self.yvgridpath))
949 elif axisname == "y3":
950 self.axes["y3"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 1),
951 lambda vy: self.vtickdirection(self.yorder, vy, 1, 1-self.yorder, vy, 1),
952 self.yvgridpath))
953 elif axisname == "y4":
954 self.axes["y4"].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 1),
955 lambda vy: self.vtickdirection(1-self.yorder, vy, 1, self.yorder, vy, 1),
956 self.yvgridpath))
957 elif axisname == "z":
958 self.axes["z"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 0, vz),
959 lambda vz: self.vtickdirection(0, 0, vz, 1, 1, vz),
960 self.zvgridpath))
961 elif axisname == "z2":
962 self.axes["z2"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 0, vz),
963 lambda vz: self.vtickdirection(1, 0, vz, 0, 1, vz),
964 self.zvgridpath))
965 elif axisname == "z3":
966 self.axes["z3"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 1, vz),
967 lambda vz: self.vtickdirection(0, 1, vz, 1, 0, vz),
968 self.zvgridpath))
969 elif axisname == "z4":
970 self.axes["z4"].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 1, vz),
971 lambda vz: self.vtickdirection(1, 1, vz, 0, 0, vz),
972 self.zvgridpath))
973 else:
974 raise NotImplementedError("4 axis per dimension supported only")
976 def dolayout(self):
977 if self.did(self.dolayout):
978 return
979 for axisname in self.axes.keys():
980 self.doaxiscreate(axisname)
982 def dobackground(self):
983 if self.did(self.dobackground):
984 return
986 def doaxes(self):
987 if self.did(self.doaxes):
988 return
989 self.dolayout()
990 self.dobackground()
991 for axis in self.axes.values():
992 self.layer("axes").insert(axis.canvas)
994 def dokey(self):
995 if self.did(self.dokey):
996 return
997 self.dobackground()
998 for plotitem in self.plotitems:
999 self.dokeyitem(plotitem)
1000 if self.key is not None:
1001 c = self.key.paint(self.keyitems)
1002 bbox = c.bbox()
1003 def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside):
1004 ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist)
1005 cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist)
1006 return ppos-cpos
1007 if bbox:
1008 x = parentchildalign(self.xpos_pt, self.xpos_pt+self.size_pt,
1009 bbox.llx_pt, bbox.urx_pt,
1010 self.key.hpos, unit.topt(self.key.hdist), self.key.hinside)
1011 y = parentchildalign(self.ypos_pt, self.ypos_pt+self.size_pt,
1012 bbox.lly_pt, bbox.ury_pt,
1013 self.key.vpos, unit.topt(self.key.vdist), self.key.vinside)
1014 self.insert(c, [trafo.translate_pt(x, y)])