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
, trafo
, unit
27 from pyx
.graph
import style
28 from pyx
.graph
.axis
import axis
, positioner
31 goldenmean
= 0.5 * (math
.sqrt(5) + 1)
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."""
48 def __init__(self
, graph
, data
, styles
):
50 self
.title
= data
.title
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
60 defaultprovider
= style
.getdefaultprovider(n
)
61 addstyles
.append(defaultprovider
)
62 provided
.extend(defaultprovider
.providesdata
)
63 provided
.extend(s
.providesdata
)
64 styles
= addstyles
+ 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
)
97 for columnname
in self
.usedcolumnnames
:
99 useitems
.append((columnname
, self
.dynamiccolumns
[columnname
]))
101 useitems
.append((columnname
, self
.data
.columns
[columnname
]))
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:
123 elif len(stylesdata
) == 1:
125 raise AttributeError("access to styledata attribute '%s' failed" % attr
)
128 class graph(canvas
.canvas
):
131 canvas
.canvas
.__init
__(self
)
139 def did(self
, method
, *args
, **kwargs
):
140 if not self
._calls
.has_key(method
):
141 self
._calls
[method
] = []
142 for callargs
in self
._calls
[method
]:
143 if callargs
== (args
, kwargs
):
145 self
._calls
[method
].append((args
, kwargs
))
150 return canvas
.canvas
.bbox(self
)
152 def registerPS(self
, registry
):
154 canvas
.canvas
.registerPS(self
, registry
)
156 def registerPDF(self
, registry
):
158 canvas
.canvas
.registerPDF(self
, registry
)
160 def processPS(self
, file, writer
, context
, registry
, bbox
):
162 canvas
.canvas
.processPS(self
, file, writer
, context
, registry
, bbox
)
164 def processPDF(self
, file, writer
, context
, registry
, bbox
):
166 canvas
.canvas
.processPDF(self
, file, writer
, context
, registry
, bbox
)
168 def plot(self
, data
, styles
=None, rangewarning
=1):
169 if self
.didranges
and rangewarning
:
170 warnings
.warn("axes ranges have already been analysed; no further adjustments will be performed")
172 raise RuntimeError("can't plot further data after dostyles() has been executed")
185 styles
= d
.defaultstyles
186 elif styles
!= d
.defaultstyles
:
187 raise RuntimeError("defaultstyles differ")
190 plotitems
.append(plotitem(self
, d
, styles
))
191 self
.plotitems
.extend(plotitems
)
193 for aplotitem
in plotitems
:
194 aplotitem
.makedynamicdata(self
)
201 if self
.did(self
.doranges
):
203 for plotitem
in self
.plotitems
:
204 plotitem
.adjustaxesstatic(self
)
205 for plotitem
in self
.plotitems
:
206 plotitem
.makedynamicdata(self
)
207 for plotitem
in self
.plotitems
:
208 plotitem
.adjustaxesdynamic(self
)
211 def doaxiscreate(self
, axisname
):
212 if self
.did(self
.doaxiscreate
, axisname
):
214 self
.doaxispositioner(axisname
)
215 self
.axes
[axisname
].create()
218 raise NotImplementedError
220 def dobackground(self
):
224 raise NotImplementedError
227 if self
.did(self
.dostyles
):
232 # count the usage of styles and perform selects
234 def stylesid(styles
):
235 return ":".join([str(id(style
)) for style
in styles
])
236 for plotitem
in self
.plotitems
:
238 styletotal
[stylesid(plotitem
.styles
)] += 1
240 styletotal
[stylesid(plotitem
.styles
)] = 1
242 for plotitem
in self
.plotitems
:
244 styleindex
[stylesid(plotitem
.styles
)] += 1
246 styleindex
[stylesid(plotitem
.styles
)] = 0
247 plotitem
.selectstyles(self
, styleindex
[stylesid(plotitem
.styles
)],
248 styletotal
[stylesid(plotitem
.styles
)])
252 def doplotitem(self
, plotitem
):
253 if self
.did(self
.doplotitem
, plotitem
):
259 for plotitem
in self
.plotitems
:
260 self
.doplotitem(plotitem
)
263 warnings
.warn("dodata() has been deprecated. Use doplot() instead.")
266 def dokeyitem(self
, plotitem
):
267 if self
.did(self
.dokeyitem
, plotitem
):
270 if plotitem
.title
is not None:
271 self
.keyitems
.append(plotitem
)
274 raise NotImplementedError
283 class graphxy(graph
):
285 def __init__(self
, xpos
=0, ypos
=0, width
=None, height
=None, ratio
=goldenmean
,
286 key
=None, backgroundattrs
=None, axesdist
=0.8*unit
.v_cm
,
287 xaxisat
=None, yaxisat
=None, **axes
):
292 self
.xpos_pt
= unit
.topt(self
.xpos
)
293 self
.ypos_pt
= unit
.topt(self
.ypos
)
294 self
.xaxisat
= xaxisat
295 self
.yaxisat
= yaxisat
297 self
.backgroundattrs
= backgroundattrs
298 self
.axesdist_pt
= unit
.topt(axesdist
)
304 raise ValueError("specify width and/or height")
306 self
.width
= ratio
* self
.height
308 self
.height
= (1.0/ratio
) * self
.width
309 self
.width_pt
= unit
.topt(self
.width
)
310 self
.height_pt
= unit
.topt(self
.height
)
312 for axisname
, aaxis
in axes
.items():
313 if aaxis
is not None:
314 if not isinstance(aaxis
, axis
.linkedaxis
):
315 self
.axes
[axisname
] = axis
.anchoredaxis(aaxis
, self
.texrunner
, axisname
)
317 self
.axes
[axisname
] = aaxis
318 for axisname
, axisat
in [("x", xaxisat
), ("y", yaxisat
)]:
319 okey
= axisname
+ "2"
320 if not axes
.has_key(axisname
):
321 if not axes
.has_key(okey
) or axes
[okey
] is None:
322 self
.axes
[axisname
] = axis
.anchoredaxis(axis
.linear(), self
.texrunner
, axisname
)
323 if not axes
.has_key(okey
):
324 self
.axes
[okey
] = axis
.linkedaxis(self
.axes
[axisname
], okey
)
326 self
.axes
[axisname
] = axis
.linkedaxis(self
.axes
[okey
], axisname
)
327 elif not axes
.has_key(okey
) and axisat
is None:
328 self
.axes
[okey
] = axis
.linkedaxis(self
.axes
[axisname
], okey
)
330 if self
.axes
.has_key("x"):
331 self
.xbasepath
= self
.axes
["x"].basepath
332 self
.xvbasepath
= self
.axes
["x"].vbasepath
333 self
.xgridpath
= self
.axes
["x"].gridpath
334 self
.xtickpoint_pt
= self
.axes
["x"].tickpoint_pt
335 self
.xtickpoint
= self
.axes
["x"].tickpoint
336 self
.xvtickpoint_pt
= self
.axes
["x"].vtickpoint_pt
337 self
.xvtickpoint
= self
.axes
["x"].tickpoint
338 self
.xtickdirection
= self
.axes
["x"].tickdirection
339 self
.xvtickdirection
= self
.axes
["x"].vtickdirection
341 if self
.axes
.has_key("y"):
342 self
.ybasepath
= self
.axes
["y"].basepath
343 self
.yvbasepath
= self
.axes
["y"].vbasepath
344 self
.ygridpath
= self
.axes
["y"].gridpath
345 self
.ytickpoint_pt
= self
.axes
["y"].tickpoint_pt
346 self
.ytickpoint
= self
.axes
["y"].tickpoint
347 self
.yvtickpoint_pt
= self
.axes
["y"].vtickpoint_pt
348 self
.yvtickpoint
= self
.axes
["y"].vtickpoint
349 self
.ytickdirection
= self
.axes
["y"].tickdirection
350 self
.yvtickdirection
= self
.axes
["y"].vtickdirection
352 self
.axesnames
= ([], [])
353 for axisname
, aaxis
in self
.axes
.items():
354 if axisname
[0] not in "xy" or (len(axisname
) != 1 and (not axisname
[1:].isdigit() or
355 axisname
[1:] == "1")):
356 raise ValueError("invalid axis name")
357 if axisname
[0] == "x":
358 self
.axesnames
[0].append(axisname
)
360 self
.axesnames
[1].append(axisname
)
361 aaxis
.setcreatecall(self
.doaxiscreate
, axisname
)
364 def pos_pt(self
, x
, y
, xaxis
=None, yaxis
=None):
366 xaxis
= self
.axes
["x"]
368 yaxis
= self
.axes
["y"]
369 return (self
.xpos_pt
+ xaxis
.convert(x
)*self
.width_pt
,
370 self
.ypos_pt
+ yaxis
.convert(y
)*self
.height_pt
)
372 def pos(self
, x
, y
, xaxis
=None, yaxis
=None):
374 xaxis
= self
.axes
["x"]
376 yaxis
= self
.axes
["y"]
377 return (self
.xpos
+ xaxis
.convert(x
)*self
.width
,
378 self
.ypos
+ yaxis
.convert(y
)*self
.height
)
380 def vpos_pt(self
, vx
, vy
):
381 return (self
.xpos_pt
+ vx
*self
.width_pt
,
382 self
.ypos_pt
+ vy
*self
.height_pt
)
384 def vpos(self
, vx
, vy
):
385 return (self
.xpos
+ vx
*self
.width
,
386 self
.ypos
+ vy
*self
.height
)
388 def vzindex(self
, vx
, vy
):
391 def vangle(self
, vx1
, vy1
, vx2
, vy2
, vx3
, vy3
):
394 def vgeodesic(self
, vx1
, vy1
, vx2
, vy2
):
395 """returns a geodesic path between two points in graph coordinates"""
396 return path
.line_pt(self
.xpos_pt
+ vx1
*self
.width_pt
,
397 self
.ypos_pt
+ vy1
*self
.height_pt
,
398 self
.xpos_pt
+ vx2
*self
.width_pt
,
399 self
.ypos_pt
+ vy2
*self
.height_pt
)
401 def vgeodesic_el(self
, vx1
, vy1
, vx2
, vy2
):
402 """returns a geodesic path element between two points in graph coordinates"""
403 return path
.lineto_pt(self
.xpos_pt
+ vx2
*self
.width_pt
,
404 self
.ypos_pt
+ vy2
*self
.height_pt
)
406 def vcap_pt(self
, coordinate
, length_pt
, vx
, vy
):
407 """returns an error cap path for a given coordinate, lengths and
408 point in graph coordinates"""
410 return path
.line_pt(self
.xpos_pt
+ vx
*self
.width_pt
- 0.5*length_pt
,
411 self
.ypos_pt
+ vy
*self
.height_pt
,
412 self
.xpos_pt
+ vx
*self
.width_pt
+ 0.5*length_pt
,
413 self
.ypos_pt
+ vy
*self
.height_pt
)
414 elif coordinate
== 1:
415 return path
.line_pt(self
.xpos_pt
+ vx
*self
.width_pt
,
416 self
.ypos_pt
+ vy
*self
.height_pt
- 0.5*length_pt
,
417 self
.xpos_pt
+ vx
*self
.width_pt
,
418 self
.ypos_pt
+ vy
*self
.height_pt
+ 0.5*length_pt
)
420 raise ValueError("direction invalid")
422 def xvgridpath(self
, vx
):
423 return path
.line_pt(self
.xpos_pt
+ vx
*self
.width_pt
, self
.ypos_pt
,
424 self
.xpos_pt
+ vx
*self
.width_pt
, self
.ypos_pt
+ self
.height_pt
)
426 def yvgridpath(self
, vy
):
427 return path
.line_pt(self
.xpos_pt
, self
.ypos_pt
+ vy
*self
.height_pt
,
428 self
.xpos_pt
+ self
.width_pt
, self
.ypos_pt
+ vy
*self
.height_pt
)
430 def axistrafo(self
, axis
, t
):
431 c
= canvas
.canvas([t
])
432 c
.insert(axis
.canvas
)
435 def axisatv(self
, axis
, v
):
436 if axis
.positioner
.fixtickdirection
[0]:
438 self
.axistrafo(axis
, trafo
.translate_pt(self
.xpos_pt
+ v
*self
.width_pt
- axis
.positioner
.x1_pt
, 0))
441 self
.axistrafo(axis
, trafo
.translate_pt(0, self
.ypos_pt
+ v
*self
.height_pt
- axis
.positioner
.y1_pt
))
443 def doaxispositioner(self
, axisname
):
444 if self
.did(self
.doaxispositioner
, axisname
):
448 self
.axes
["x"].setpositioner(positioner
.lineaxispos_pt(self
.xpos_pt
, self
.ypos_pt
,
449 self
.xpos_pt
+ self
.width_pt
, self
.ypos_pt
,
450 (0, 1), self
.xvgridpath
))
451 elif axisname
== "x2":
452 self
.axes
["x2"].setpositioner(positioner
.lineaxispos_pt(self
.xpos_pt
, self
.ypos_pt
+ self
.height_pt
,
453 self
.xpos_pt
+ self
.width_pt
, self
.ypos_pt
+ self
.height_pt
,
454 (0, -1), self
.xvgridpath
))
455 elif axisname
== "y":
456 self
.axes
["y"].setpositioner(positioner
.lineaxispos_pt(self
.xpos_pt
, self
.ypos_pt
,
457 self
.xpos_pt
, self
.ypos_pt
+ self
.height_pt
,
458 (1, 0), self
.yvgridpath
))
459 elif axisname
== "y2":
460 self
.axes
["y2"].setpositioner(positioner
.lineaxispos_pt(self
.xpos_pt
+ self
.width_pt
, self
.ypos_pt
,
461 self
.xpos_pt
+ self
.width_pt
, self
.ypos_pt
+ self
.height_pt
,
462 (-1, 0), self
.yvgridpath
))
464 if axisname
[1:] == "3":
465 dependsonaxisname
= axisname
[0]
467 dependsonaxisname
= "%s%d" % (axisname
[0], int(axisname
[1:]) - 2)
468 self
.doaxiscreate(dependsonaxisname
)
469 sign
= 2*(int(axisname
[1:]) % 2) - 1
470 if axisname
[0] == "x":
471 y_pt
= self
.axes
[dependsonaxisname
].positioner
.y1_pt
- sign
* (self
.axes
[dependsonaxisname
].canvas
.extent_pt
+ self
.axesdist_pt
)
472 self
.axes
[axisname
].setpositioner(positioner
.lineaxispos_pt(self
.xpos_pt
, y_pt
,
473 self
.xpos_pt
+ self
.width_pt
, y_pt
,
474 (0, sign
), self
.xvgridpath
))
476 x_pt
= self
.axes
[dependsonaxisname
].positioner
.x1_pt
- sign
* (self
.axes
[dependsonaxisname
].canvas
.extent_pt
+ self
.axesdist_pt
)
477 self
.axes
[axisname
].setpositioner(positioner
.lineaxispos_pt(x_pt
, self
.ypos_pt
,
478 x_pt
, self
.ypos_pt
+ self
.height_pt
,
479 (sign
, 0), self
.yvgridpath
))
482 if self
.did(self
.dolayout
):
484 for axisname
in self
.axes
.keys():
485 self
.doaxiscreate(axisname
)
486 if self
.xaxisat
is not None:
487 self
.axisatv(self
.axes
["x"], self
.axes
["y"].convert(self
.xaxisat
))
488 if self
.yaxisat
is not None:
489 self
.axisatv(self
.axes
["y"], self
.axes
["x"].convert(self
.yaxisat
))
491 def dobackground(self
):
492 if self
.did(self
.dobackground
):
494 if self
.backgroundattrs
is not None:
495 self
.draw(path
.rect_pt(self
.xpos_pt
, self
.ypos_pt
, self
.width_pt
, self
.height_pt
),
496 self
.backgroundattrs
)
499 if self
.did(self
.doaxes
):
503 for axis
in self
.axes
.values():
504 self
.insert(axis
.canvas
)
507 if self
.did(self
.dokey
):
510 for plotitem
in self
.plotitems
:
511 self
.dokeyitem(plotitem
)
512 if self
.key
is not None:
513 c
= self
.key
.paint(self
.keyitems
)
515 def parentchildalign(pmin
, pmax
, cmin
, cmax
, pos
, dist
, inside
):
516 ppos
= pmin
+0.5*(cmax
-cmin
)+dist
+pos
*(pmax
-pmin
-cmax
+cmin
-2*dist
)
517 cpos
= 0.5*(cmin
+cmax
)+(1-inside
)*(1-2*pos
)*(cmax
-cmin
+2*dist
)
520 x
= parentchildalign(self
.xpos_pt
, self
.xpos_pt
+self
.width_pt
,
521 bbox
.llx_pt
, bbox
.urx_pt
,
522 self
.key
.hpos
, unit
.topt(self
.key
.hdist
), self
.key
.hinside
)
523 y
= parentchildalign(self
.ypos_pt
, self
.ypos_pt
+self
.height_pt
,
524 bbox
.lly_pt
, bbox
.ury_pt
,
525 self
.key
.vpos
, unit
.topt(self
.key
.vdist
), self
.key
.vinside
)
526 self
.insert(c
, [trafo
.translate_pt(x
, y
)])
529 class graphxyz(graphxy
):
533 def __init__(self
, distance
, phi
, theta
, anglefactor
=math
.pi
/180):
536 self
.distance
= distance
538 self
.a
= (-math
.sin(phi
), math
.cos(phi
), 0)
539 self
.b
= (-math
.cos(phi
)*math
.sin(theta
),
540 -math
.sin(phi
)*math
.sin(theta
),
542 self
.eye
= (distance
*math
.cos(phi
)*math
.cos(theta
),
543 distance
*math
.sin(phi
)*math
.cos(theta
),
544 distance
*math
.sin(theta
))
546 def point(self
, x
, y
, z
):
547 d0
= (self
.a
[0]*self
.b
[1]*(z
-self
.eye
[2])
548 + self
.a
[2]*self
.b
[0]*(y
-self
.eye
[1])
549 + self
.a
[1]*self
.b
[2]*(x
-self
.eye
[0])
550 - self
.a
[2]*self
.b
[1]*(x
-self
.eye
[0])
551 - self
.a
[0]*self
.b
[2]*(y
-self
.eye
[1])
552 - self
.a
[1]*self
.b
[0]*(z
-self
.eye
[2]))
553 da
= (self
.eye
[0]*self
.b
[1]*(z
-self
.eye
[2])
554 + self
.eye
[2]*self
.b
[0]*(y
-self
.eye
[1])
555 + self
.eye
[1]*self
.b
[2]*(x
-self
.eye
[0])
556 - self
.eye
[2]*self
.b
[1]*(x
-self
.eye
[0])
557 - self
.eye
[0]*self
.b
[2]*(y
-self
.eye
[1])
558 - self
.eye
[1]*self
.b
[0]*(z
-self
.eye
[2]))
559 db
= (self
.a
[0]*self
.eye
[1]*(z
-self
.eye
[2])
560 + self
.a
[2]*self
.eye
[0]*(y
-self
.eye
[1])
561 + self
.a
[1]*self
.eye
[2]*(x
-self
.eye
[0])
562 - self
.a
[2]*self
.eye
[1]*(x
-self
.eye
[0])
563 - self
.a
[0]*self
.eye
[2]*(y
-self
.eye
[1])
564 - self
.a
[1]*self
.eye
[0]*(z
-self
.eye
[2]))
567 def zindex(self
, x
, y
, z
):
568 return math
.sqrt((x
-self
.eye
[0])*(x
-self
.eye
[0])+(y
-self
.eye
[1])*(y
-self
.eye
[1])+(z
-self
.eye
[2])*(z
-self
.eye
[2]))-self
.distance
570 def angle(self
, x1
, y1
, z1
, x2
, y2
, z2
, x3
, y3
, z3
):
571 sx
= (x1
-self
.eye
[0])
572 sy
= (y1
-self
.eye
[1])
573 sz
= (z1
-self
.eye
[2])
574 nx
= (y2
-y1
)*(z3
-z1
)-(z2
-z1
)*(y3
-y1
)
575 ny
= (z2
-z1
)*(x3
-x1
)-(x2
-x1
)*(z3
-z1
)
576 nz
= (x2
-x1
)*(y3
-y1
)-(y2
-y1
)*(x3
-x1
)
577 return (sx
*nx
+sy
*ny
+sz
*nz
)/math
.sqrt(nx
*nx
+ny
*ny
+nz
*nz
)/math
.sqrt(sx
*sx
+sy
*sy
+sz
*sz
)
582 def __init__(self
, phi
, theta
, anglefactor
=math
.pi
/180):
586 self
.a
= (-math
.sin(phi
), math
.cos(phi
), 0)
587 self
.b
= (-math
.cos(phi
)*math
.sin(theta
),
588 -math
.sin(phi
)*math
.sin(theta
),
590 self
.c
= (-math
.cos(phi
)*math
.cos(theta
),
591 -math
.sin(phi
)*math
.cos(theta
),
594 def point(self
, x
, y
, z
):
595 return self
.a
[0]*x
+self
.a
[1]*y
+self
.a
[2]*z
, self
.b
[0]*x
+self
.b
[1]*y
+self
.b
[2]*z
597 def zindex(self
, x
, y
, z
):
598 return self
.c
[0]*x
+self
.c
[1]*y
+self
.c
[2]*z
600 def angle(self
, x1
, y1
, z1
, x2
, y2
, z2
, x3
, y3
, z3
):
601 nx
= (y2
-y1
)*(z3
-z1
)-(z2
-z1
)*(y3
-y1
)
602 ny
= (z2
-z1
)*(x3
-x1
)-(x2
-x1
)*(z3
-z1
)
603 nz
= (x2
-x1
)*(y3
-y1
)-(y2
-y1
)*(x3
-x1
)
604 return (self
.c
[0]*nx
+self
.c
[1]*ny
+self
.c
[2]*nz
)/math
.sqrt(nx
*nx
+ny
*ny
+nz
*nz
)
607 def __init__(self
, xpos
=0, ypos
=0, size
=None,
608 xscale
=1, yscale
=1, zscale
=1/goldenmean
,
609 projector
=central(10, -30, 30), key
=None,
616 self
.xpos_pt
= unit
.topt(xpos
)
617 self
.ypos_pt
= unit
.topt(ypos
)
618 self
.size_pt
= unit
.topt(size
)
622 self
.projector
= projector
625 self
.xorder
= projector
.zindex(0, -1, 0) > projector
.zindex(0, 1, 0) and 1 or 0
626 self
.yorder
= projector
.zindex(-1, 0, 0) > projector
.zindex(1, 0, 0) and 1 or 0
627 self
.zindexscale
= math
.sqrt(xscale
*xscale
+yscale
*yscale
+zscale
*zscale
)
629 for axisname
, aaxis
in axes
.items():
630 if aaxis
is not None:
631 if not isinstance(aaxis
, axis
.linkedaxis
):
632 self
.axes
[axisname
] = axis
.anchoredaxis(aaxis
, self
.texrunner
, axisname
)
634 self
.axes
[axisname
] = aaxis
635 for axisname
in ["x", "y"]:
636 okey
= axisname
+ "2"
637 if not axes
.has_key(axisname
):
638 if not axes
.has_key(okey
) or axes
[okey
] is None:
639 self
.axes
[axisname
] = axis
.anchoredaxis(axis
.linear(), self
.texrunner
, axisname
)
640 if not axes
.has_key(okey
):
641 self
.axes
[okey
] = axis
.linkedaxis(self
.axes
[axisname
], okey
)
643 self
.axes
[axisname
] = axis
.linkedaxis(self
.axes
[okey
], axisname
)
644 if not axes
.has_key("z"):
645 self
.axes
["z"] = axis
.anchoredaxis(axis
.linear(), self
.texrunner
, "z")
647 if self
.axes
.has_key("x"):
648 self
.xbasepath
= self
.axes
["x"].basepath
649 self
.xvbasepath
= self
.axes
["x"].vbasepath
650 self
.xgridpath
= self
.axes
["x"].gridpath
651 self
.xtickpoint_pt
= self
.axes
["x"].tickpoint_pt
652 self
.xtickpoint
= self
.axes
["x"].tickpoint
653 self
.xvtickpoint_pt
= self
.axes
["x"].vtickpoint_pt
654 self
.xvtickpoint
= self
.axes
["x"].tickpoint
655 self
.xtickdirection
= self
.axes
["x"].tickdirection
656 self
.xvtickdirection
= self
.axes
["x"].vtickdirection
658 if self
.axes
.has_key("y"):
659 self
.ybasepath
= self
.axes
["y"].basepath
660 self
.yvbasepath
= self
.axes
["y"].vbasepath
661 self
.ygridpath
= self
.axes
["y"].gridpath
662 self
.ytickpoint_pt
= self
.axes
["y"].tickpoint_pt
663 self
.ytickpoint
= self
.axes
["y"].tickpoint
664 self
.yvtickpoint_pt
= self
.axes
["y"].vtickpoint_pt
665 self
.yvtickpoint
= self
.axes
["y"].vtickpoint
666 self
.ytickdirection
= self
.axes
["y"].tickdirection
667 self
.yvtickdirection
= self
.axes
["y"].vtickdirection
669 if self
.axes
.has_key("z"):
670 self
.zbasepath
= self
.axes
["z"].basepath
671 self
.zvbasepath
= self
.axes
["z"].vbasepath
672 self
.zgridpath
= self
.axes
["z"].gridpath
673 self
.ztickpoint_pt
= self
.axes
["z"].tickpoint_pt
674 self
.ztickpoint
= self
.axes
["z"].tickpoint
675 self
.zvtickpoint_pt
= self
.axes
["z"].vtickpoint
676 self
.zvtickpoint
= self
.axes
["z"].vtickpoint
677 self
.ztickdirection
= self
.axes
["z"].tickdirection
678 self
.zvtickdirection
= self
.axes
["z"].vtickdirection
680 self
.axesnames
= ([], [], [])
681 for axisname
, aaxis
in self
.axes
.items():
682 if axisname
[0] not in "xyz" or (len(axisname
) != 1 and (not axisname
[1:].isdigit() or
683 axisname
[1:] == "1")):
684 raise ValueError("invalid axis name")
685 if axisname
[0] == "x":
686 self
.axesnames
[0].append(axisname
)
687 elif axisname
[0] == "y":
688 self
.axesnames
[1].append(axisname
)
690 self
.axesnames
[2].append(axisname
)
691 aaxis
.setcreatecall(self
.doaxiscreate
, axisname
)
693 def pos_pt(self
, x
, y
, z
, xaxis
=None, yaxis
=None, zaxis
=None):
695 xaxis
= self
.axes
["x"]
697 yaxis
= self
.axes
["y"]
699 zaxis
= self
.axes
["z"]
700 return self
.vpos_pt(xaxis
.convert(x
), yaxis
.convert(y
), zaxis
.convert(z
))
702 def pos(self
, x
, y
, z
, xaxis
=None, yaxis
=None, zaxis
=None):
704 xaxis
= self
.axes
["x"]
706 yaxis
= self
.axes
["y"]
708 zaxis
= self
.axes
["z"]
709 return self
.vpos(xaxis
.convert(x
), yaxis
.convert(y
), zaxis
.convert(z
))
711 def vpos_pt(self
, vx
, vy
, vz
):
712 x
, y
= self
.projector
.point(2*self
.xscale
*(vx
- 0.5),
713 2*self
.yscale
*(vy
- 0.5),
714 2*self
.zscale
*(vz
- 0.5))
715 return self
.xpos_pt
+x
*self
.size_pt
, self
.ypos_pt
+y
*self
.size_pt
717 def vpos(self
, vx
, vy
, vz
):
718 x
, y
= self
.projector
.point(2*self
.xscale
*(vx
- 0.5),
719 2*self
.yscale
*(vy
- 0.5),
720 2*self
.zscale
*(vz
- 0.5))
721 return self
.xpos
+x
*self
.size
, self
.ypos
+y
*self
.size
723 def vzindex(self
, vx
, vy
, vz
):
724 return self
.projector
.zindex(2*self
.xscale
*(vx
- 0.5),
725 2*self
.yscale
*(vy
- 0.5),
726 2*self
.zscale
*(vz
- 0.5))/self
.zindexscale
728 def vangle(self
, vx1
, vy1
, vz1
, vx2
, vy2
, vz2
, vx3
, vy3
, vz3
):
729 return self
.projector
.angle(2*self
.xscale
*(vx1
- 0.5),
730 2*self
.yscale
*(vy1
- 0.5),
731 2*self
.zscale
*(vz1
- 0.5),
732 2*self
.xscale
*(vx2
- 0.5),
733 2*self
.yscale
*(vy2
- 0.5),
734 2*self
.zscale
*(vz2
- 0.5),
735 2*self
.xscale
*(vx3
- 0.5),
736 2*self
.yscale
*(vy3
- 0.5),
737 2*self
.zscale
*(vz3
- 0.5))
739 def vgeodesic(self
, vx1
, vy1
, vz1
, vx2
, vy2
, vz2
):
740 """returns a geodesic path between two points in graph coordinates"""
741 return path
.line_pt(*(self
.vpos_pt(vx1
, vy1
, vz1
) + self
.vpos_pt(vx2
, vy2
, vz2
)))
743 def vgeodesic_el(self
, vx1
, vy1
, vz1
, vx2
, vy2
, vz2
):
744 """returns a geodesic path element between two points in graph coordinates"""
745 return path
.lineto_pt(*self
.vpos_pt(vx2
, vy2
, vz2
))
747 def vcap_pt(self
, coordinate
, length_pt
, vx
, vy
, vz
):
748 """returns an error cap path for a given coordinate, lengths and
749 point in graph coordinates"""
751 return self
.vgeodesic(vx
-0.5*length_pt
/self
.size_pt
, vy
, vz
, vx
+0.5*length_pt
/self
.size_pt
, vy
, vz
)
752 elif coordinate
== 1:
753 return self
.vgeodesic(vx
, vy
-0.5*length_pt
/self
.size_pt
, vz
, vx
, vy
+0.5*length_pt
/self
.size_pt
, vz
)
754 elif coordinate
== 2:
755 return self
.vgeodesic(vx
, vy
, vz
-0.5*length_pt
/self
.size_pt
, vx
, vy
, vz
+0.5*length_pt
/self
.size_pt
)
757 raise ValueError("direction invalid")
759 def xvtickdirection(self
, vx
):
761 x1_pt
, y1_pt
= self
.vpos_pt(vx
, 1, 0)
762 x2_pt
, y2_pt
= self
.vpos_pt(vx
, 0, 0)
764 x1_pt
, y1_pt
= self
.vpos_pt(vx
, 0, 0)
765 x2_pt
, y2_pt
= self
.vpos_pt(vx
, 1, 0)
766 dx_pt
= x2_pt
- x1_pt
767 dy_pt
= y2_pt
- y1_pt
768 norm
= math
.hypot(dx_pt
, dy_pt
)
769 return dx_pt
/norm
, dy_pt
/norm
771 def yvtickdirection(self
, vy
):
773 x1_pt
, y1_pt
= self
.vpos_pt(1, vy
, 0)
774 x2_pt
, y2_pt
= self
.vpos_pt(0, vy
, 0)
776 x1_pt
, y1_pt
= self
.vpos_pt(0, vy
, 0)
777 x2_pt
, y2_pt
= self
.vpos_pt(1, vy
, 0)
778 dx_pt
= x2_pt
- x1_pt
779 dy_pt
= y2_pt
- y1_pt
780 norm
= math
.hypot(dx_pt
, dy_pt
)
781 return dx_pt
/norm
, dy_pt
/norm
783 def vtickdirection(self
, vx1
, vy1
, vz1
, vx2
, vy2
, vz2
):
784 x1_pt
, y1_pt
= self
.vpos_pt(vx1
, vy1
, vz1
)
785 x2_pt
, y2_pt
= self
.vpos_pt(vx2
, vy2
, vz2
)
786 dx_pt
= x2_pt
- x1_pt
787 dy_pt
= y2_pt
- y1_pt
788 norm
= math
.hypot(dx_pt
, dy_pt
)
789 return dx_pt
/norm
, dy_pt
/norm
791 def xvgridpath(self
, vx
):
792 return path
.path(path
.moveto_pt(*self
.vpos_pt(vx
, 0, 0)),
793 path
.lineto_pt(*self
.vpos_pt(vx
, 1, 0)),
794 path
.lineto_pt(*self
.vpos_pt(vx
, 1, 1)),
795 path
.lineto_pt(*self
.vpos_pt(vx
, 0, 1)),
798 def yvgridpath(self
, vy
):
799 return path
.path(path
.moveto_pt(*self
.vpos_pt(0, vy
, 0)),
800 path
.lineto_pt(*self
.vpos_pt(1, vy
, 0)),
801 path
.lineto_pt(*self
.vpos_pt(1, vy
, 1)),
802 path
.lineto_pt(*self
.vpos_pt(0, vy
, 1)),
805 def zvgridpath(self
, vz
):
806 return path
.path(path
.moveto_pt(*self
.vpos_pt(0, 0, vz
)),
807 path
.lineto_pt(*self
.vpos_pt(1, 0, vz
)),
808 path
.lineto_pt(*self
.vpos_pt(1, 1, vz
)),
809 path
.lineto_pt(*self
.vpos_pt(0, 1, vz
)),
812 def doaxispositioner(self
, axisname
):
813 if self
.did(self
.doaxispositioner
, axisname
):
817 self
.axes
["x"].setpositioner(positioner
.flexlineaxispos_pt(lambda vx
: self
.vpos_pt(vx
, self
.xorder
, 0),
818 lambda vx
: self
.vtickdirection(vx
, self
.xorder
, 0, vx
, 1-self
.xorder
, 0),
820 elif axisname
== "x2":
821 self
.axes
["x2"].setpositioner(positioner
.flexlineaxispos_pt(lambda vx
: self
.vpos_pt(vx
, 1-self
.xorder
, 0),
822 lambda vx
: self
.vtickdirection(vx
, 1-self
.xorder
, 0, vx
, self
.xorder
, 0),
824 elif axisname
== "x3":
825 self
.axes
["x3"].setpositioner(positioner
.flexlineaxispos_pt(lambda vx
: self
.vpos_pt(vx
, self
.xorder
, 1),
826 lambda vx
: self
.vtickdirection(vx
, self
.xorder
, 1, vx
, 1-self
.xorder
, 1),
828 elif axisname
== "x4":
829 self
.axes
["x4"].setpositioner(positioner
.flexlineaxispos_pt(lambda vx
: self
.vpos_pt(vx
, 1-self
.xorder
, 1),
830 lambda vx
: self
.vtickdirection(vx
, 1-self
.xorder
, 1, vx
, self
.xorder
, 1),
832 elif axisname
== "y":
833 self
.axes
["y"].setpositioner(positioner
.flexlineaxispos_pt(lambda vy
: self
.vpos_pt(self
.yorder
, vy
, 0),
834 lambda vy
: self
.vtickdirection(self
.yorder
, vy
, 0, 1-self
.yorder
, vy
, 0),
836 elif axisname
== "y2":
837 self
.axes
["y2"].setpositioner(positioner
.flexlineaxispos_pt(lambda vy
: self
.vpos_pt(1-self
.yorder
, vy
, 0),
838 lambda vy
: self
.vtickdirection(1-self
.yorder
, vy
, 0, self
.yorder
, vy
, 0),
840 elif axisname
== "y3":
841 self
.axes
["y3"].setpositioner(positioner
.flexlineaxispos_pt(lambda vy
: self
.vpos_pt(self
.yorder
, vy
, 1),
842 lambda vy
: self
.vtickdirection(self
.yorder
, vy
, 1, 1-self
.yorder
, vy
, 1),
844 elif axisname
== "y4":
845 self
.axes
["y4"].setpositioner(positioner
.flexlineaxispos_pt(lambda vy
: self
.vpos_pt(1-self
.yorder
, vy
, 1),
846 lambda vy
: self
.vtickdirection(1-self
.yorder
, vy
, 1, self
.yorder
, vy
, 1),
848 elif axisname
== "z":
849 self
.axes
["z"].setpositioner(positioner
.flexlineaxispos_pt(lambda vz
: self
.vpos_pt(0, 0, vz
),
850 lambda vz
: self
.vtickdirection(0, 0, vz
, 1, 1, vz
),
852 elif axisname
== "z2":
853 self
.axes
["z2"].setpositioner(positioner
.flexlineaxispos_pt(lambda vz
: self
.vpos_pt(1, 0, vz
),
854 lambda vz
: self
.vtickdirection(1, 0, vz
, 0, 1, vz
),
856 elif axisname
== "z3":
857 self
.axes
["z3"].setpositioner(positioner
.flexlineaxispos_pt(lambda vz
: self
.vpos_pt(0, 1, vz
),
858 lambda vz
: self
.vtickdirection(0, 1, vz
, 1, 0, vz
),
860 elif axisname
== "z4":
861 self
.axes
["z4"].setpositioner(positioner
.flexlineaxispos_pt(lambda vz
: self
.vpos_pt(1, 1, vz
),
862 lambda vz
: self
.vtickdirection(1, 1, vz
, 0, 0, vz
),
865 raise NotImplementedError("4 axis per dimension supported only")
868 if self
.did(self
.dolayout
):
870 for axisname
in self
.axes
.keys():
871 self
.doaxiscreate(axisname
)
873 def dobackground(self
):
874 if self
.did(self
.dobackground
):
878 if self
.did(self
.doaxes
):
882 for axis
in self
.axes
.values():
883 self
.insert(axis
.canvas
)
886 if self
.did(self
.dokey
):
889 for plotitem
in self
.plotitems
:
890 self
.dokeyitem(plotitem
)
891 if self
.key
is not None:
892 c
= self
.key
.paint(self
.keyitems
)
894 def parentchildalign(pmin
, pmax
, cmin
, cmax
, pos
, dist
, inside
):
895 ppos
= pmin
+0.5*(cmax
-cmin
)+dist
+pos
*(pmax
-pmin
-cmax
+cmin
-2*dist
)
896 cpos
= 0.5*(cmin
+cmax
)+(1-inside
)*(1-2*pos
)*(cmax
-cmin
+2*dist
)
899 x
= parentchildalign(self
.xpos_pt
, self
.xpos_pt
+self
.size_pt
,
900 bbox
.llx_pt
, bbox
.urx_pt
,
901 self
.key
.hpos
, unit
.topt(self
.key
.hdist
), self
.key
.hinside
)
902 y
= parentchildalign(self
.ypos_pt
, self
.ypos_pt
+self
.size_pt
,
903 bbox
.lly_pt
, bbox
.ury_pt
,
904 self
.key
.vpos
, unit
.topt(self
.key
.vdist
), self
.key
.vinside
)
905 self
.insert(c
, [trafo
.translate_pt(x
, y
)])