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.
39 def registerdefaultprovider(style
, keys
):
40 """sets a style as a default creator for sharedata variables '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
]
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."""
67 def __init__(self
, graph
, data
, styles
):
69 self
.title
= data
.title
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
79 defaultprovider
= getdefaultprovider(n
)
80 addstyles
.append(defaultprovider
)
81 provided
.extend(defaultprovider
.providesdata
)
82 provided
.extend(s
.providesdata
)
83 styles
= addstyles
+ 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
)
117 for columnname
in self
.usedcolumnnames
:
119 useitems
.append((columnname
, self
.dynamiccolumns
[columnname
]))
121 useitems
.append((columnname
, self
.data
.columns
[columnname
]))
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:
145 elif len(stylesdata
) == 1:
147 raise AttributeError("access to styledata attribute '%s' failed" % attr
)
150 class graph(canvas
.canvas
):
153 canvas
.canvas
.__init
__(self
)
154 for name
in ["background", "filldata", "axes.grid", "axis.baseline", "axis.ticks", "axis.labels", "axis.title", "data", "key"]:
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
):
169 self
._calls
[method
].append((args
, kwargs
))
174 return canvas
.canvas
.bbox(self
)
177 def registerPS(self
, registry
):
179 canvas
.canvas
.registerPS(self
, registry
)
181 def registerPDF(self
, registry
):
183 canvas
.canvas
.registerPDF(self
, registry
)
185 def processPS(self
, file, writer
, context
, registry
, bbox
):
187 canvas
.canvas
.processPS(self
, file, writer
, context
, registry
, bbox
)
189 def processPDF(self
, file, writer
, context
, registry
, bbox
):
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")
197 raise RuntimeError("can't plot further data after dostyles() has been executed")
210 styles
= d
.defaultstyles
211 elif styles
!= d
.defaultstyles
:
212 raise RuntimeError("defaultstyles differ")
215 plotitems
.append(plotitem(self
, d
, styles
))
216 self
.plotitems
.extend(plotitems
)
218 for aplotitem
in plotitems
:
219 aplotitem
.makedynamicdata(self
)
226 if self
.did(self
.doranges
):
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
)
236 def doaxiscreate(self
, axisname
):
237 if self
.did(self
.doaxiscreate
, axisname
):
239 self
.doaxispositioner(axisname
)
240 self
.axes
[axisname
].create()
243 raise NotImplementedError
245 def dobackground(self
):
249 raise NotImplementedError
252 if self
.did(self
.dostyles
):
257 # count the usage of styles and perform selects
259 def stylesid(styles
):
260 return ":".join([str(id(style
)) for style
in styles
])
261 for plotitem
in self
.plotitems
:
263 styletotal
[stylesid(plotitem
.styles
)] += 1
265 styletotal
[stylesid(plotitem
.styles
)] = 1
267 for plotitem
in self
.plotitems
:
269 styleindex
[stylesid(plotitem
.styles
)] += 1
271 styleindex
[stylesid(plotitem
.styles
)] = 0
272 plotitem
.selectstyles(self
, styleindex
[stylesid(plotitem
.styles
)],
273 styletotal
[stylesid(plotitem
.styles
)])
277 def doplotitem(self
, plotitem
):
278 if self
.did(self
.doplotitem
, plotitem
):
284 for plotitem
in self
.plotitems
:
285 self
.doplotitem(plotitem
)
288 warnings
.warn("dodata() has been deprecated. Use doplot() instead.")
291 def dokeyitem(self
, plotitem
):
292 if self
.did(self
.dokeyitem
, plotitem
):
295 if plotitem
.title
is not None:
296 self
.keyitems
.append(plotitem
)
299 raise NotImplementedError
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
):
317 self
.xpos_pt
= unit
.topt(self
.xpos
)
318 self
.ypos_pt
= unit
.topt(self
.ypos
)
319 self
.xaxisat
= xaxisat
320 self
.yaxisat
= yaxisat
322 self
.backgroundattrs
= backgroundattrs
323 self
.axesdist_pt
= unit
.topt(axesdist
)
324 self
.flipped
= flipped
330 raise ValueError("specify width and/or height")
332 self
.width
= ratio
* self
.height
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
)
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
)
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
)
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
))
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):
409 xaxis
= self
.axes
["x"]
411 yaxis
= self
.axes
["y"]
412 vx
= xaxis
.convert(x
)
413 vy
= yaxis
.convert(y
)
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):
421 xaxis
= self
.axes
["x"]
423 yaxis
= self
.axes
["y"]
424 vx
= xaxis
.convert(x
)
425 vy
= yaxis
.convert(y
)
428 return (self
.xpos
+ vx
*self
.width
,
429 self
.ypos
+ vy
*self
.height
)
431 def vpos_pt(self
, vx
, vy
):
434 return (self
.xpos_pt
+ vx
*self
.width_pt
,
435 self
.ypos_pt
+ vy
*self
.height_pt
)
437 def vpos(self
, vx
, vy
):
440 return (self
.xpos
+ vx
*self
.width
,
441 self
.ypos
+ vy
*self
.height
)
443 def vzindex(self
, vx
, vy
):
446 def vangle(self
, vx1
, vy1
, vx2
, vy2
, vx3
, vy3
):
449 def vgeodesic(self
, vx1
, vy1
, vx2
, vy2
):
450 """returns a geodesic path between two points in graph coordinates"""
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"""
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"""
471 coordinate
= 1-coordinate
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
)
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]:
497 t
= trafo
.translate_pt(self
.xpos_pt
+ v
*self
.width_pt
- axis
.positioner
.x1_pt
, 0)
500 t
= trafo
.translate_pt(0, self
.ypos_pt
+ v
*self
.height_pt
- axis
.positioner
.y1_pt
)
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
)
507 def doaxispositioner(self
, axisname
):
508 if self
.did(self
.doaxispositioner
, axisname
):
511 if axisname
in ["x", "x2", "y", "y2"]:
512 self
.axes
[axisname
].setpositioner(self
.axespositioners
[axisname
])
514 if axisname
[1:] == "3":
515 dependsonaxisname
= axisname
[0]
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
:
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
))
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
))
534 if self
.did(self
.dolayout
):
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
):
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
)
551 if self
.did(self
.doaxes
):
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
)
561 if self
.did(self
.dokey
):
564 for plotitem
in self
.plotitems
:
565 self
.dokeyitem(plotitem
)
566 if self
.key
is not None:
567 c
= self
.key
.paint(self
.keyitems
)
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
)
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
):
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
)
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)
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
):
623 return graphxy
.vcap_pt(self
, coordinate
, length_pt
, vx
, 0.5)
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
):
642 def __init__(self
, distance
, phi
, theta
, anglefactor
=math
.pi
/180):
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
),
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]))
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
)
691 def __init__(self
, phi
, theta
, anglefactor
=math
.pi
/180):
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
),
699 self
.c
= (-math
.cos(phi
)*math
.cos(theta
),
700 -math
.sin(phi
)*math
.cos(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,
725 self
.xpos_pt
= unit
.topt(xpos
)
726 self
.ypos_pt
= unit
.topt(ypos
)
727 self
.size_pt
= unit
.topt(size
)
731 self
.projector
= projector
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
)
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
)
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
)
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):
804 xaxis
= self
.axes
["x"]
806 yaxis
= self
.axes
["y"]
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):
813 xaxis
= self
.axes
["x"]
815 yaxis
= self
.axes
["y"]
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"""
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
)
866 raise ValueError("direction invalid")
868 def xvtickdirection(self
, vx
):
870 x1_pt
, y1_pt
= self
.vpos_pt(vx
, 1, 0)
871 x2_pt
, y2_pt
= self
.vpos_pt(vx
, 0, 0)
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
):
882 x1_pt
, y1_pt
= self
.vpos_pt(1, vy
, 0)
883 x2_pt
, y2_pt
= self
.vpos_pt(0, vy
, 0)
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)),
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)),
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
)),
921 def doaxispositioner(self
, axisname
):
922 if self
.did(self
.doaxispositioner
, axisname
):
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),
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),
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),
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),
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),
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),
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),
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),
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
),
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
),
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
),
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
),
974 raise NotImplementedError("4 axis per dimension supported only")
977 if self
.did(self
.dolayout
):
979 for axisname
in self
.axes
.keys():
980 self
.doaxiscreate(axisname
)
982 def dobackground(self
):
983 if self
.did(self
.dobackground
):
987 if self
.did(self
.doaxes
):
991 for axis
in self
.axes
.values():
992 self
.layer("axes").insert(axis
.canvas
)
995 if self
.did(self
.dokey
):
998 for plotitem
in self
.plotitems
:
999 self
.dokeyitem(plotitem
)
1000 if self
.key
is not None:
1001 c
= self
.key
.paint(self
.keyitems
)
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
)
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
)])