some code reorganisation/renaming: separate graph directory, write(-Postscript) metho...
[PyX/mjg.git] / pyx / graph / painter.py
blobfc11b56e2fa98e0df994596fe11417b153194186
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 import math
27 from pyx import canvas, color, attr, text, style, unit, box, path
28 from pyx import trafo as trafomodule
29 from pyx.graph import parter
32 goldenmean = 0.5 * (math.sqrt(5) + 1)
35 class axiscanvas(canvas.canvas):
36 """axis canvas
37 - an axis canvas is a regular canvas returned by an
38 axispainters painter method
39 - it contains a PyX length extent to be used for the
40 alignment of additional axes; the axis extent should
41 be handled by the axispainters painter method; you may
42 apprehend this as a size information comparable to a
43 bounding box, which must be handled manually
44 - it contains a list of textboxes called labels which are
45 used to rate the distances between the labels if needed
46 by the axis later on; the painter method has not only to
47 insert the labels into this canvas, but should also fill
48 this list, when a rating of the distances should be
49 performed by the axis"""
51 # __implements__ = sole implementation
53 def __init__(self, *args, **kwargs):
54 """initializes the instance
55 - sets extent to zero
56 - sets labels to an empty list"""
57 canvas.canvas.__init__(self, *args, **kwargs)
58 self.extent = 0
59 self.labels = []
62 class rotatetext:
63 """create rotations accordingly to tick directions
64 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
66 # __implements__ = sole implementation
68 def __init__(self, direction, epsilon=1e-10):
69 """initializes the instance
70 - direction is an angle to be used relative to the tick direction
71 - epsilon is the value by which 90 degrees can be exceeded before
72 an 180 degree rotation is added"""
73 self.direction = direction
74 self.epsilon = epsilon
76 def trafo(self, dx, dy):
77 """returns a rotation transformation accordingly to the tick direction
78 - dx and dy are the direction of the tick"""
79 direction = self.direction + math.atan2(dy, dx) * 180 / math.pi
80 while (direction > 180 + self.epsilon):
81 direction -= 360
82 while (direction < -180 - self.epsilon):
83 direction += 360
84 while (direction > 90 + self.epsilon):
85 direction -= 180
86 while (direction < -90 - self.epsilon):
87 direction += 180
88 return trafomodule.rotate(direction)
91 rotatetext.parallel = rotatetext(90)
92 rotatetext.orthogonal = rotatetext(180)
95 class _Iaxispainter:
96 "class for painting axes"
98 def paint(self, axispos, axis, ac=None):
99 """paint the axis into an axiscanvas
100 - returns the axiscanvas
101 - when no axiscanvas is provided (the typical case), a new
102 axiscanvas is created. however, when extending an painter
103 by inheritance, painting on the same axiscanvas is supported
104 by setting the axiscanvas attribute
105 - axispos is an instance, which implements _Iaxispos to
106 define the tick positions
107 - the axis and should not be modified (we may
108 add some temporary variables like axis.ticks[i].temp_xxx,
109 which might be used just temporary) -- the idea is that
110 all things can be used several times
111 - also do not modify the instance (self) -- even this
112 instance might be used several times; thus do not modify
113 attributes like self.titleattrs etc. (use local copies)
114 - the method might access some additional attributes from
115 the axis, e.g. the axis title -- the axis painter should
116 document this behavior and rely on the availability of
117 those attributes -> it becomes a question of the proper
118 usage of the combination of axis & axispainter
119 - the axiscanvas is a axiscanvas instance and should be
120 filled with ticks, labels, title, etc.; note that the
121 extent and labels instance variables should be handled
122 as documented in the axiscanvas"""
125 class _Iaxispos:
126 """interface definition of axis tick position methods
127 - these methods are used for the postitioning of the ticks
128 when painting an axis"""
129 # TODO: should we add a local transformation (for label text etc?)
130 # (this might replace tickdirection (and even tickposition?))
132 def basepath(self, x1=None, x2=None):
133 """return the basepath as a path
134 - x1 is the start position; if not set, the basepath starts
135 from the beginning of the axis, which might imply a
136 value outside of the graph coordinate range [0; 1]
137 - x2 is analogous to x1, but for the end position"""
139 def vbasepath(self, v1=None, v2=None):
140 """return the basepath as a path
141 - like basepath, but for graph coordinates"""
143 def gridpath(self, x):
144 """return the gridpath as a path for a given position x
145 - might return None when no gridpath is available"""
147 def vgridpath(self, v):
148 """return the gridpath as a path for a given position v
149 in graph coordinates
150 - might return None when no gridpath is available"""
152 def tickpoint_pt(self, x):
153 """return the position at the basepath as a tuple (x, y) in
154 postscript points for the position x"""
156 def tickpoint(self, x):
157 """return the position at the basepath as a tuple (x, y) in
158 in PyX length for the position x"""
160 def vtickpoint_pt(self, v):
161 "like tickpoint_pt, but for graph coordinates"
163 def vtickpoint(self, v):
164 "like tickpoint, but for graph coordinates"
166 def tickdirection(self, x):
167 """return the direction of a tick as a tuple (dx, dy) for the
168 position x (the direction points towards the graph)"""
170 def vtickdirection(self, v):
171 """like tickposition, but for graph coordinates"""
174 class _axispos:
175 """implements those parts of _Iaxispos which can be build
176 out of the axis convert method and other _Iaxispos methods
177 - base _Iaxispos methods, which need to be implemented:
178 - vbasepath
179 - vgridpath
180 - vtickpoint_pt
181 - vtickdirection
182 - other methods needed for _Iaxispos are build out of those
183 listed above when this class is inherited"""
185 def __init__(self, convert):
186 """initializes the instance
187 - convert is a convert method from an axis"""
188 self.convert = convert
190 def basepath(self, x1=None, x2=None):
191 if x1 is None:
192 if x2 is None:
193 return self.vbasepath()
194 else:
195 return self.vbasepath(v2=self.convert(x2))
196 else:
197 if x2 is None:
198 return self.vbasepath(v1=self.convert(x1))
199 else:
200 return self.vbasepath(v1=self.convert(x1), v2=self.convert(x2))
202 def gridpath(self, x):
203 return self.vgridpath(self.convert(x))
205 def tickpoint_pt(self, x):
206 return self.vtickpoint_pt(self.convert(x))
208 def tickpoint(self, x):
209 return self.vtickpoint(self.convert(x))
211 def vtickpoint(self, v):
212 return [unit.t_pt(x) for x in self.vtickpoint(v)]
214 def tickdirection(self, x):
215 return self.vtickdirection(self.convert(x))
218 class pathaxispos(_axispos):
219 """axis tick position methods along an arbitrary path"""
221 __implements__ = _Iaxispos
223 def __init__(self, p, convert, direction=1):
224 self.path = p
225 self.normpath = path.normpath(p)
226 self.arclength_pt = self.normpath.arclength_pt()
227 self.arclength = unit.t_pt(self.arclength_pt)
228 _axispos.__init__(self, convert)
229 self.direction = direction
231 def vbasepath(self, v1=None, v2=None):
232 if v1 is None:
233 if v2 is None:
234 return self.path
235 else:
236 return self.normpath.split(self.normpath.lentopar(v2 * self.arclength))[0]
237 else:
238 if v2 is None:
239 return self.normpath.split(self.normpath.lentopar(v1 * self.arclength))[1]
240 else:
241 return self.normpath.split(*self.normpath.lentopar([v1 * self.arclength, v2 * self.arclength]))[1]
243 def vgridpath(self, v):
244 return None
246 def vtickpoint_pt(self, v):
247 return self.normpath.at_pt(self.normpath.lentopar(v * self.arclength))
249 def vtickdirection(self, v):
250 t = self.normpath.tangent(self.normpath.lentopar(v * self.arclength))
251 tbegin = t.begin_pt()
252 tend = t.end_pt()
253 dx = tend[0]-tbegin[0]
254 dy = tend[1]-tbegin[1]
255 norm = math.sqrt(dx*dx + dy*dy)
256 if self.direction == 1:
257 return -dy/norm, dx/norm
258 elif self.direction == -1:
259 return dy/norm, -dx/norm
260 raise RuntimeError("unknown direction")
263 class axistitlepainter:
264 """class for painting an axis title
265 - the axis must have a title attribute when using this painter;
266 this title might be None"""
268 __implements__ = _Iaxispainter
270 defaulttitleattrs = [text.halign.center, text.vshift.mathaxis]
272 def __init__(self, titledist="0.3 cm",
273 titleattrs=[],
274 titledirection=rotatetext.parallel,
275 titlepos=0.5,
276 texrunner=text.defaulttexrunner):
277 """initialized the instance
278 - titledist is a visual PyX length giving the distance
279 of the title from the axis extent already there (a title might
280 be added after labels or other things are plotted already)
281 - titleattrs is a list of attributes for a texrunners text
282 method; a single is allowed without being a list; None
283 turns off the title
284 - titledirection is an instance of rotatetext or None
285 - titlepos is the position of the title in graph coordinates
286 - texrunner is the texrunner to be used to create text
287 (the texrunner is available for further use in derived
288 classes as instance variable texrunner)"""
289 self.titledist_str = titledist
290 self.titleattrs = titleattrs
291 self.titledirection = titledirection
292 self.titlepos = titlepos
293 self.texrunner = texrunner
295 def paint(self, axispos, axis, ac=None):
296 if ac is None:
297 ac = axiscanvas()
298 if axis.title is not None and self.titleattrs is not None:
299 titledist = unit.length(self.titledist_str, default_type="v")
300 x, y = axispos.vtickpoint_pt(self.titlepos)
301 dx, dy = axispos.vtickdirection(self.titlepos)
302 titleattrs = self.defaulttitleattrs + self.titleattrs
303 if self.titledirection is not None:
304 titleattrs.append(self.titledirection.trafo(dx, dy))
305 title = self.texrunner.text_pt(x, y, axis.title, titleattrs)
306 ac.extent += titledist
307 title.linealign(ac.extent, -dx, -dy)
308 ac.extent += title.extent(dx, dy)
309 ac.insert(title)
310 return ac
313 class geometricseries(attr.changeattr):
315 def __init__(self, initial, factor):
316 self.initial = initial
317 self.factor = factor
319 def select(self, index, total):
320 return self.initial * (self.factor ** index)
323 class ticklength(geometricseries): pass
325 _base = 0.2
327 #ticklength.short = ticklength("%f cm" % (_base/math.sqrt(64)), 1/goldenmean)
328 ticklength.short = ticklength(_base/math.sqrt(64), 1/goldenmean)
329 ticklength.short = ticklength(_base/math.sqrt(32), 1/goldenmean)
330 ticklength.short = ticklength(_base/math.sqrt(16), 1/goldenmean)
331 ticklength.short = ticklength(_base/math.sqrt(8), 1/goldenmean)
332 ticklength.short = ticklength(_base/math.sqrt(4), 1/goldenmean)
333 ticklength.short = ticklength(_base/math.sqrt(2), 1/goldenmean)
334 ticklength.normal = ticklength(_base, 1/goldenmean)
335 ticklength.long = ticklength(_base*math.sqrt(2), 1/goldenmean)
336 ticklength.long = ticklength(_base*math.sqrt(4), 1/goldenmean)
337 ticklength.long = ticklength(_base*math.sqrt(8), 1/goldenmean)
338 ticklength.long = ticklength(_base*math.sqrt(16), 1/goldenmean)
339 ticklength.long = ticklength(_base*math.sqrt(32), 1/goldenmean)
342 class axispainter(axistitlepainter):
343 """class for painting the ticks and labels of an axis
344 - the inherited titleaxispainter is used to paint the title of
345 the axis
346 - note that the type of the elements of ticks given as an argument
347 of the paint method must be suitable for the tick position methods
348 of the axis"""
350 __implements__ = _Iaxispainter
352 defaulttickattrs = []
353 defaultgridattrs = []
354 defaultbasepathattrs = [style.linecap.square]
355 defaultlabelattrs = [text.halign.center, text.vshift.mathaxis]
357 def __init__(self, innerticklength=ticklength.short,
358 outerticklength=None,
359 tickattrs=[],
360 gridattrs=None,
361 basepathattrs=[],
362 labeldist="0.3 cm",
363 labelattrs=[],
364 labeldirection=None,
365 labelhequalize=0,
366 labelvequalize=1,
367 **kwargs):
368 """initializes the instance
369 - innerticklength and outerticklength are two lists of
370 visual PyX lengths for ticks, subticks, etc. plotted inside
371 and outside of the graph; when a single value is given, it
372 is used for all tick levels; None turns off ticks inside or
373 outside of the graph
374 - tickattrs are a list of stroke attributes for the ticks;
375 a single entry is allowed without being a list; None turns
376 off ticks
377 - gridattrs are a list of lists used as stroke
378 attributes for ticks, subticks etc.; when a single list
379 is given, it is used for ticks, subticks, etc.; a single
380 entry is allowed without being a list; None turns off
381 the grid
382 - basepathattrs are a list of stroke attributes for a grid
383 line at axis value zero; a single entry is allowed without
384 being a list; None turns off the basepath
385 - labeldist is a visual PyX length for the distance of the labels
386 from the axis basepath
387 - labelattrs is a list of attributes for a texrunners text
388 method; a single entry is allowed without being a list;
389 None turns off the labels
390 - titledirection is an instance of rotatetext or None
391 - labelhequalize and labelvequalize (booleans) perform an equal
392 alignment for straight vertical and horizontal axes, respectively
393 - futher keyword arguments are passed to axistitlepainter"""
394 # TODO: access to axis.divisor -- document, remove, ... ???
395 self.innerticklength_str = innerticklength
396 self.outerticklength_str = outerticklength
397 self.tickattrs = tickattrs
398 self.gridattrs = gridattrs
399 self.basepathattrs = basepathattrs
400 self.labeldist_str = labeldist
401 self.labelattrs = labelattrs
402 self.labeldirection = labeldirection
403 self.labelhequalize = labelhequalize
404 self.labelvequalize = labelvequalize
405 axistitlepainter.__init__(self, **kwargs)
407 def paint(self, axispos, axis, ac=None):
408 if ac is None:
409 ac = axiscanvas()
410 labeldist = unit.length(self.labeldist_str, default_type="v")
411 for tick in axis.ticks:
412 tick.temp_v = axis.convert(float(tick) * axis.divisor)
413 tick.temp_x, tick.temp_y = axispos.vtickpoint_pt(tick.temp_v)
414 tick.temp_dx, tick.temp_dy = axispos.vtickdirection(tick.temp_v)
415 maxticklevel, maxlabellevel = parter._maxlevels(axis.ticks)
417 # create & align tick.temp_labelbox
418 for tick in axis.ticks:
419 if tick.labellevel is not None:
420 labelattrs = attr.selectattrs(self.labelattrs, tick.labellevel, maxlabellevel)
421 if labelattrs is not None:
422 labelattrs = self.defaultlabelattrs + labelattrs
423 if self.labeldirection is not None:
424 labelattrs.append(self.labeldirection.trafo(tick.temp_dx, tick.temp_dy))
425 if tick.labelattrs is not None:
426 labelattrs.extend(tick.labelattrs)
427 tick.temp_labelbox = self.texrunner.text_pt(tick.temp_x, tick.temp_y, tick.label, labelattrs)
428 if len(axis.ticks) > 1:
429 equaldirection = 1
430 for tick in axis.ticks[1:]:
431 if tick.temp_dx != axis.ticks[0].temp_dx or tick.temp_dy != axis.ticks[0].temp_dy:
432 equaldirection = 0
433 else:
434 equaldirection = 0
435 if equaldirection and ((not axis.ticks[0].temp_dx and self.labelvequalize) or
436 (not axis.ticks[0].temp_dy and self.labelhequalize)):
437 if self.labelattrs is not None:
438 box.linealignequal([tick.temp_labelbox for tick in axis.ticks if tick.labellevel is not None],
439 labeldist, -axis.ticks[0].temp_dx, -axis.ticks[0].temp_dy)
440 else:
441 for tick in axis.ticks:
442 if tick.labellevel is not None and self.labelattrs is not None:
443 tick.temp_labelbox.linealign(labeldist, -tick.temp_dx, -tick.temp_dy)
445 for tick in axis.ticks:
446 if tick.ticklevel is not None:
447 innerticklength = attr.selectattr(self.innerticklength_str, tick.ticklevel, maxticklevel)
448 outerticklength = attr.selectattr(self.outerticklength_str, tick.ticklevel, maxticklevel)
449 if innerticklength is not None or outerticklength is not None:
450 if innerticklength is None:
451 innerticklength = 0
452 else:
453 innerticklength = unit.length(innerticklength, default_type="v")
454 if outerticklength is None:
455 outerticklength = 0
456 else:
457 outerticklength = unit.length(outerticklength, default_type="v")
458 tickattrs = attr.selectattrs(self.defaulttickattrs + self.tickattrs, tick.ticklevel, maxticklevel)
459 if tickattrs is not None:
460 innerticklength_pt = unit.topt(innerticklength)
461 outerticklength_pt = unit.topt(outerticklength)
462 x1 = tick.temp_x + tick.temp_dx * innerticklength_pt
463 y1 = tick.temp_y + tick.temp_dy * innerticklength_pt
464 x2 = tick.temp_x - tick.temp_dx * outerticklength_pt
465 y2 = tick.temp_y - tick.temp_dy * outerticklength_pt
466 ac.stroke(path.line_pt(x1, y1, x2, y2), tickattrs)
467 if outerticklength is not None and unit.topt(outerticklength) > unit.topt(ac.extent):
468 ac.extent = outerticklength
469 if outerticklength is not None and unit.topt(-innerticklength) > unit.topt(ac.extent):
470 ac.extent = -innerticklength
471 if self.gridattrs is not None:
472 gridattrs = attr.selectattrs(self.defaultgridattrs + self.gridattrs, tick.ticklevel, maxticklevel)
473 ac.stroke(axispos.vgridpath(tick.temp_v), gridattrs)
474 if tick.labellevel is not None and self.labelattrs is not None:
475 ac.insert(tick.temp_labelbox)
476 ac.labels.append(tick.temp_labelbox)
477 extent = tick.temp_labelbox.extent(tick.temp_dx, tick.temp_dy) + labeldist
478 if unit.topt(extent) > unit.topt(ac.extent):
479 ac.extent = extent
480 if self.basepathattrs is not None:
481 ac.stroke(axispos.vbasepath(), self.defaultbasepathattrs + self.basepathattrs)
483 # for tick in axis.ticks:
484 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
485 # del tick.temp_x
486 # del tick.temp_y
487 # del tick.temp_dx
488 # del tick.temp_dy
489 # if tick.labellevel is not None and self.labelattrs is not None:
490 # del tick.temp_labelbox
492 axistitlepainter.paint(self, axispos, axis, ac=ac)
494 return ac
497 class linkaxispainter(axispainter):
498 """class for painting a linked axis
499 - the inherited axispainter is used to paint the axis
500 - modifies some constructor defaults"""
502 __implements__ = _Iaxispainter
504 def __init__(self, labelattrs=None,
505 titleattrs=None,
506 **kwargs):
507 """initializes the instance
508 - the labelattrs default is set to None thus skipping the labels
509 - the titleattrs default is set to None thus skipping the title
510 - all keyword arguments are passed to axispainter"""
511 axispainter.__init__(self, labelattrs=labelattrs,
512 titleattrs=titleattrs,
513 **kwargs)
516 class subaxispos:
517 """implementation of the _Iaxispos interface for a subaxis"""
519 __implements__ = _Iaxispos
521 def __init__(self, convert, baseaxispos, vmin, vmax, vminover, vmaxover):
522 """initializes the instance
523 - convert is the subaxis convert method
524 - baseaxispos is the axispos instance of the base axis
525 - vmin, vmax is the range covered by the subaxis in graph coordinates
526 - vminover, vmaxover is the extended range of the subaxis including
527 regions between several subaxes (for basepath drawing etc.)"""
528 self.convert = convert
529 self.baseaxispos = baseaxispos
530 self.vmin = vmin
531 self.vmax = vmax
532 self.vminover = vminover
533 self.vmaxover = vmaxover
535 def basepath(self, x1=None, x2=None):
536 if x1 is not None:
537 v1 = self.vmin+self.convert(x1)*(self.vmax-self.vmin)
538 else:
539 v1 = self.vminover
540 if x2 is not None:
541 v2 = self.vmin+self.convert(x2)*(self.vmax-self.vmin)
542 else:
543 v2 = self.vmaxover
544 return self.baseaxispos.vbasepath(v1, v2)
546 def vbasepath(self, v1=None, v2=None):
547 if v1 is not None:
548 v1 = self.vmin+v1*(self.vmax-self.vmin)
549 else:
550 v1 = self.vminover
551 if v2 is not None:
552 v2 = self.vmin+v2*(self.vmax-self.vmin)
553 else:
554 v2 = self.vmaxover
555 return self.baseaxispos.vbasepath(v1, v2)
557 def gridpath(self, x):
558 return self.baseaxispos.vgridpath(self.vmin+self.convert(x)*(self.vmax-self.vmin))
560 def vgridpath(self, v):
561 return self.baseaxispos.vgridpath(self.vmin+v*(self.vmax-self.vmin))
563 def tickpoint_pt(self, x, axis=None):
564 return self.baseaxispos.vtickpoint_pt(self.vmin+self.convert(x)*(self.vmax-self.vmin))
566 def tickpoint(self, x, axis=None):
567 return self.baseaxispos.vtickpoint(self.vmin+self.convert(x)*(self.vmax-self.vmin))
569 def vtickpoint_pt(self, v, axis=None):
570 return self.baseaxispos.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
572 def vtickpoint(self, v, axis=None):
573 return self.baseaxispos.vtickpoint(self.vmin+v*(self.vmax-self.vmin))
575 def tickdirection(self, x, axis=None):
576 return self.baseaxispos.vtickdirection(self.vmin+self.convert(x)*(self.vmax-self.vmin))
578 def vtickdirection(self, v, axis=None):
579 return self.baseaxispos.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
582 class splitaxispainter(axistitlepainter):
583 """class for painting a splitaxis
584 - the inherited titleaxispainter is used to paint the title of
585 the axis
586 - the splitaxispainter access the subaxes attribute of the axis"""
588 __implements__ = _Iaxispainter
590 defaultbreaklinesattrs = []
592 def __init__(self, breaklinesdist="0.05 cm",
593 breaklineslength="0.5 cm",
594 breaklinesangle=-60,
595 breaklinesattrs=[],
596 **args):
597 """initializes the instance
598 - breaklinesdist is a visual length of the distance between
599 the two lines of the axis break
600 - breaklineslength is a visual length of the length of the
601 two lines of the axis break
602 - breaklinesangle is the angle of the lines of the axis break
603 - breaklinesattrs are a list of stroke attributes for the
604 axis break lines; a single entry is allowed without being a
605 list; None turns off the break lines
606 - futher keyword arguments are passed to axistitlepainter"""
607 self.breaklinesdist_str = breaklinesdist
608 self.breaklineslength_str = breaklineslength
609 self.breaklinesangle = breaklinesangle
610 self.breaklinesattrs = breaklinesattrs
611 axistitlepainter.__init__(self, **args)
613 def paint(self, axispos, axis, ac=None):
614 if ac is None:
615 ac = axiscanvas()
616 for subaxis in axis.subaxes:
617 subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, subaxis.vminover, subaxis.vmaxover))
618 ac.insert(subaxis.axiscanvas)
619 if unit.topt(ac.extent) < unit.topt(subaxis.axiscanvas.extent):
620 ac.extent = subaxis.axiscanvas.extent
621 if self.breaklinesattrs is not None:
622 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
623 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
624 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
625 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
626 breaklinesextent = (0.5*self.breaklinesdist*math.fabs(self.cos) +
627 0.5*self.breaklineslength*math.fabs(self.sin))
628 if unit.topt(ac.extent) < unit.topt(breaklinesextent):
629 ac.extent = breaklinesextent
630 for subaxis1, subaxis2 in zip(axis.subaxes[:-1], axis.subaxes[1:]):
631 # use a tangent of the basepath (this is independent of the tickdirection)
632 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
633 p = path.normpath(axispos.vbasepath(v, None))
634 breakline = p.tangent(0, self.breaklineslength)
635 widthline = p.tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
636 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
637 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
638 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
639 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
640 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
641 ac.fill(path.path(path.moveto_pt(*breakline1.begin_pt()),
642 path.lineto_pt(*breakline1.end_pt()),
643 path.lineto_pt(*breakline2.end_pt()),
644 path.lineto_pt(*breakline2.begin_pt()),
645 path.closepath()), [color.gray.white])
646 ac.stroke(breakline1, self.defaultbreaklinesattrs + self.breaklinesattrs)
647 ac.stroke(breakline2, self.defaultbreaklinesattrs + self.breaklinesattrs)
648 axistitlepainter.paint(self, axispos, axis, ac=ac)
649 return ac
652 class linksplitaxispainter(splitaxispainter):
653 """class for painting a linked splitaxis
654 - the inherited splitaxispainter is used to paint the axis
655 - modifies some constructor defaults"""
657 __implements__ = _Iaxispainter
659 def __init__(self, titleattrs=None, **kwargs):
660 """initializes the instance
661 - the titleattrs default is set to None thus skipping the title
662 - all keyword arguments are passed to splitaxispainter"""
663 splitaxispainter.__init__(self, titleattrs=titleattrs, **kwargs)
666 class baraxispainter(axistitlepainter):
667 """class for painting a baraxis
668 - the inherited titleaxispainter is used to paint the title of
669 the axis
670 - the baraxispainter access the multisubaxis, subaxis names, texts, and
671 relsizes attributes"""
673 __implements__ = _Iaxispainter
675 defaulttickattrs = []
676 defaultbasepathattrs = [style.linecap.square]
677 defaultnameattrs = [text.halign.center, text.vshift.mathaxis]
679 def __init__(self, innerticklength=None,
680 outerticklength=None,
681 tickattrs=[],
682 basepathattrs=[],
683 namedist="0.3 cm",
684 nameattrs=[],
685 namedirection=None,
686 namepos=0.5,
687 namehequalize=0,
688 namevequalize=1,
689 **args):
690 """initializes the instance
691 - innerticklength and outerticklength are a visual length of
692 the ticks to be plotted at the axis basepath to visually
693 separate the bars; if neither innerticklength nor
694 outerticklength are set, not ticks are plotted
695 - breaklinesattrs are a list of stroke attributes for the
696 axis tick; a single entry is allowed without being a
697 list; None turns off the ticks
698 - namedist is a visual PyX length for the distance of the bar
699 names from the axis basepath
700 - nameattrs is a list of attributes for a texrunners text
701 method; a single entry is allowed without being a list;
702 None turns off the names
703 - namedirection is an instance of rotatetext or None
704 - namehequalize and namevequalize (booleans) perform an equal
705 alignment for straight vertical and horizontal axes, respectively
706 - futher keyword arguments are passed to axistitlepainter"""
707 self.innerticklength_str = innerticklength
708 self.outerticklength_str = outerticklength
709 self.tickattrs = tickattrs
710 self.basepathattrs = basepathattrs
711 self.namedist_str = namedist
712 self.nameattrs = nameattrs
713 self.namedirection = namedirection
714 self.namepos = namepos
715 self.namehequalize = namehequalize
716 self.namevequalize = namevequalize
717 axistitlepainter.__init__(self, **args)
719 def paint(self, axispos, axis, ac=None):
720 if ac is None:
721 ac = axiscanvas()
722 if axis.multisubaxis is not None:
723 for subaxis in axis.subaxis:
724 subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, None, None))
725 ac.insert(subaxis.axiscanvas)
726 if unit.topt(ac.extent) < unit.topt(subaxis.axiscanvas.extent):
727 ac.extent = subaxis.axiscanvas.extent
728 namepos = []
729 for name in axis.names:
730 v = axis.convert((name, self.namepos))
731 x, y = axispos.vtickpoint_pt(v)
732 dx, dy = axispos.vtickdirection(v)
733 namepos.append((v, x, y, dx, dy))
734 nameboxes = []
735 if self.nameattrs is not None:
736 for (v, x, y, dx, dy), name in zip(namepos, axis.names):
737 nameattrs = self.defaultnameattrs + self.nameattrs
738 if self.namedirection is not None:
739 nameattrs.append(self.namedirection.trafo(tick.temp_dx, tick.temp_dy))
740 if axis.texts.has_key(name):
741 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[name]), nameattrs))
742 elif axis.texts.has_key(str(name)):
743 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[str(name)]), nameattrs))
744 else:
745 nameboxes.append(self.texrunner.text_pt(x, y, str(name), nameattrs))
746 labeldist = ac.extent + unit.length(self.namedist_str, default_type="v")
747 if len(namepos) > 1:
748 equaldirection = 1
749 for np in namepos[1:]:
750 if np[3] != namepos[0][3] or np[4] != namepos[0][4]:
751 equaldirection = 0
752 else:
753 equaldirection = 0
754 if equaldirection and ((not namepos[0][3] and self.namevequalize) or
755 (not namepos[0][4] and self.namehequalize)):
756 box.linealignequal(nameboxes, labeldist, -namepos[0][3], -namepos[0][4])
757 else:
758 for namebox, np in zip(nameboxes, namepos):
759 namebox.linealign(labeldist, -np[3], -np[4])
760 if self.basepathattrs is not None:
761 p = axispos.vbasepath()
762 if p is not None:
763 ac.stroke(p, self.defaultbasepathattrs + self.basepathattrs)
764 if self.tickattrs is not None and (self.innerticklength_str is not None or
765 self.outerticklength_str is not None):
766 if self.innerticklength_str is not None:
767 innerticklength = unit.length(self.innerticklength_str, default_type="v")
768 innerticklength_pt = unit.topt(innerticklength)
769 if unit.topt(ac.extent) < -innerticklength_pt:
770 ac.extent = -innerticklength
771 elif self.outerticklength_str is not None:
772 innerticklength = innerticklength_pt = 0
773 if self.outerticklength_str is not None:
774 outerticklength = unit.length(self.outerticklength_str, default_type="v")
775 outerticklength_pt = unit.topt(outerticklength)
776 if unit.topt(ac.extent) < outerticklength_pt:
777 ac.extent = outerticklength
778 elif self.innerticklength_str is not None:
779 outerticklength = outerticklength_pt = 0
780 for pos in axis.relsizes:
781 if pos == axis.relsizes[0]:
782 pos -= axis.firstdist
783 elif pos != axis.relsizes[-1]:
784 pos -= 0.5 * axis.dist
785 v = pos / axis.relsizes[-1]
786 x, y = axispos.vtickpoint_pt(v)
787 dx, dy = axispos.vtickdirection(v)
788 x1 = x + dx * innerticklength_pt
789 y1 = y + dy * innerticklength_pt
790 x2 = x - dx * outerticklength_pt
791 y2 = y - dy * outerticklength_pt
792 ac.stroke(path.line_pt(x1, y1, x2, y2), self.defaulttickattrs + self.tickattrs)
793 for (v, x, y, dx, dy), namebox in zip(namepos, nameboxes):
794 newextent = namebox.extent(dx, dy) + labeldist
795 if unit.topt(ac.extent) < unit.topt(newextent):
796 ac.extent = newextent
797 for namebox in nameboxes:
798 ac.insert(namebox)
799 axistitlepainter.paint(self, axispos, axis, ac=ac)
800 return ac
803 class linkbaraxispainter(baraxispainter):
804 """class for painting a linked baraxis
805 - the inherited baraxispainter is used to paint the axis
806 - modifies some constructor defaults"""
808 __implements__ = _Iaxispainter
810 def __init__(self, nameattrs=None, titleattrs=None, **kwargs):
811 """initializes the instance
812 - the titleattrs default is set to None thus skipping the title
813 - the nameattrs default is set to None thus skipping the names
814 - all keyword arguments are passed to axispainter"""
815 baraxispainter.__init__(self, nameattrs=nameattrs, titleattrs=titleattrs, **kwargs)