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
27 from pyx
import attr
, helper
28 from pyx
.graph
.axis
import painter
, parter
, rater
, texter
, tick
32 """interface definition of a map
33 maps convert a value into another value by bijective transformation f"""
39 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
41 def setbasepoints(self
, basepoints
):
42 """set basepoints for the convertions
43 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
44 the number of basepoints needed might depend on the transformation
45 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
51 __implements__
= _Imap
53 def setbasepoints(self
, basepoints
):
54 self
.x1
= float(basepoints
[0][0])
55 self
.y1
= float(basepoints
[0][1])
56 self
.x2
= float(basepoints
[1][0])
57 self
.y2
= float(basepoints
[1][1])
58 self
.dydx
= (self
.y2
- self
.y1
) / (self
.x2
- self
.x1
)
59 self
.dxdy
= (self
.x2
- self
.x1
) / (self
.y2
- self
.y1
)
61 def convert(self
, value
):
62 return self
.y1
+ self
.dydx
* (float(value
) - self
.x1
)
64 def invert(self
, value
):
65 return self
.x1
+ self
.dxdy
* (float(value
) - self
.y1
)
70 __implements__
= _Imap
72 def setbasepoints(self
, basepoints
):
73 self
.x1
= float(math
.log(basepoints
[0][0]))
74 self
.y1
= float(basepoints
[0][1])
75 self
.x2
= float(math
.log(basepoints
[1][0]))
76 self
.y2
= float(basepoints
[1][1])
77 self
.dydx
= (self
.y2
- self
.y1
) / (self
.x2
- self
.x1
)
78 self
.dxdy
= (self
.x2
- self
.x1
) / (self
.y2
- self
.y1
)
80 def convert(self
, value
):
81 return self
.y1
+ self
.dydx
* (math
.log(float(value
)) - self
.x1
)
83 def invert(self
, value
):
84 return math
.exp(self
.x1
+ self
.dxdy
* (float(value
) - self
.y1
))
88 """interface definition of a axis
89 - an axis should implement an convert and invert method like
90 _Imap, but this is not part of this interface definition;
91 one possibility is to mix-in a proper map class, but special
92 purpose axes might do something else
93 - an axis has the instance variable axiscanvas after the finish
95 - an axis might have further instance variables (title, ticks)
96 to be used in combination with appropriate axispainters"""
99 "convert a value into graph coordinates"
102 "invert a graph coordinate to a axis value"
104 def getrelsize(self
):
105 """returns the relative size (width) of the axis
106 - for use in split axis, bar axis etc.
107 - might return None if no size is available"""
109 # TODO: describe adjustrange
110 def setrange(self
, min=None, max=None):
111 """set the axis data range
112 - the type of min and max must fit to the axis
113 - min<max; the axis might be reversed, but this is
114 expressed internally only (min<max all the time)
115 - the axis might not apply the change of the range
116 (e.g. when the axis range is fixed by the user),
117 but usually the range is extended to contain the
119 - for invalid parameters (e.g. negativ values at an
120 logarithmic axis), an exception should be raised
121 - a RuntimeError is raised, when setrange is called
122 after the finish method"""
125 """return data range as a tuple (min, max)
126 - min<max; the axis might be reversed, but this is
127 expressed internally only
128 - a RuntimeError exception is raised when no
129 range is available"""
131 def finish(self
, axispos
):
133 - axispos implements _Iaxispos
134 - sets the instance axiscanvas, which is insertable into the
135 graph to finally paint the axis
136 - any modification of the axis range should be disabled after
137 the finish method was called"""
138 # TODO: be more specific about exceptions
140 def createlinkaxis(self
, **kwargs
):
141 """create a link axis to the axis itself
142 - typically, a link axis is a axis, which share almost
143 all properties with the axis it is linked to
144 - typically, the painter gets replaced by a painter
145 which doesn't put any text to the axis"""
149 """base implementation a regular axis
150 - typical usage is to mix-in a linmap or a logmap to
151 complete the axis interface
152 - note that some methods of this class want to access a
153 parter and a rater; those attributes implementing _Iparter
154 and _Irater should be initialized by the constructors
155 of derived classes"""
157 def __init__(self
, min=None, max=None, reverse
=0, divisor
=None,
158 title
=None, painter
=painter
.regular(), texter
=texter
.mixed(),
159 density
=1, maxworse
=2, manualticks
=[]):
160 """initializes the instance
161 - min and max fix the axis minimum and maximum, respectively;
162 they are determined by the data to be plotted, when not fixed
163 - reverse (boolean) reverses the minimum and the maximum of
165 - numerical divisor for the axis partitioning
166 - title is a string containing the axis title
167 - axispainter is the axis painter (should implement _Ipainter)
168 - texter is the texter (should implement _Itexter)
169 - density is a global parameter for the axis paritioning and
170 axis rating; its default is 1, but the range 0.5 to 2.5 should
171 be usefull to get less or more ticks by the automatic axis
173 - maxworse is a number of trials with worse tick rating
174 before giving up (usually it should not be needed to increase
175 this value; increasing the number will slow down the automatic
176 axis partitioning considerably)
177 - manualticks and the partitioner results are mixed
179 - note that some methods of this class want to access a
180 parter and a rater; those attributes implementing _Iparter
181 and _Irater should be initialized by the constructors
182 of derived classes"""
183 if min is not None and max is not None and min > max:
184 min, max, reverse
= max, min, not reverse
185 self
.fixmin
, self
.fixmax
, self
.min, self
.max, self
.reverse
= min is not None, max is not None, min, max, reverse
186 self
.divisor
= divisor
189 self
.painter
= painter
191 self
.density
= density
192 self
.maxworse
= maxworse
193 self
.manualticks
= self
.checkfraclist(manualticks
)
194 self
.axiscanvas
= None
197 def _setrange(self
, min=None, max=None):
198 if not self
.fixmin
and min is not None and (self
.min is None or min < self
.min):
200 if not self
.fixmax
and max is not None and (self
.max is None or max > self
.max):
202 if None not in (self
.min, self
.max) and self
.min != self
.max:
204 if self
.usedivisor
and self
.divisor
is not None:
205 self
.setbasepoints(((self
.min/self
.divisor
, 1), (self
.max/self
.divisor
, 0)))
207 self
.setbasepoints(((self
.min, 1), (self
.max, 0)))
209 if self
.usedivisor
and self
.divisor
is not None:
210 self
.setbasepoints(((self
.min/self
.divisor
, 0), (self
.max/self
.divisor
, 1)))
212 self
.setbasepoints(((self
.min, 0), (self
.max, 1)))
215 return self
.min, self
.max
217 def _forcerange(self
, range):
218 self
.min, self
.max = range
221 def setrange(self
, min=None, max=None):
222 if self
.usedivisor
and self
.divisor
is not None:
223 min = float(self
.divisor
) * self
.divisor
224 max = float(self
.divisor
) * self
.divisor
225 oldmin
, oldmax
= self
.min, self
.max
226 self
._setrange
(min, max)
227 if self
.axiscanvas
is not None and ((oldmin
!= self
.min) or (oldmax
!= self
.max)):
228 raise RuntimeError("range modification while axis was already finished")
232 def adjustrange(self
, data
, index
, deltamindata
=None, deltaminindex
=None, deltamaxdata
=None, deltamaxindex
=None):
233 assert deltamindata
is None or deltamaxdata
is None
235 if deltamindata
is not None:
236 for point
, minpoint
in zip(data
, deltamindata
):
238 if index
is not None:
239 if deltaminindex
is not None:
240 value
= point
[index
] - minpoint
[deltaminindex
] + self
.zero
242 value
= point
[index
] - minpoint
+ self
.zero
244 if deltaminindex
is not None:
245 value
= point
- minpoint
[deltaminindex
] + self
.zero
247 value
= point
- minpoint
+ self
.zero
251 if min is None or value
< min: min = value
252 if max is None or value
> max: max = value
253 elif deltamaxdata
is not None:
254 for point
, maxpoint
in zip(data
, deltamaxdata
):
256 if index
is not None:
257 if deltamaxindex
is not None:
258 value
= point
[index
] + maxpoint
[deltamaxindex
] + self
.zero
260 value
= point
[index
] + maxpoint
+ self
.zero
262 if deltamaxindex
is not None:
263 value
= point
+ maxpoint
[deltamaxindex
] + self
.zero
265 value
= point
+ maxpoint
+ self
.zero
269 if min is None or value
< min: min = value
270 if max is None or value
> max: max = value
274 if index
is not None:
275 value
= point
[index
] + self
.zero
277 value
= point
+ self
.zero
281 if min is None or value
< min: min = value
282 if max is None or value
> max: max = value
283 self
.setrange(min, max)
286 if self
.min is not None and self
.max is not None:
287 if self
.usedivisor
and self
.divisor
is not None:
288 return float(self
.min) / self
.divisor
, float(self
.max) / self
.divisor
290 return self
.min, self
.max
292 def checkfraclist(self
, fracs
):
293 "orders a list of fracs, equal entries are not allowed"
294 if not len(fracs
): return []
298 for item
in sorted[1:]:
300 raise ValueError("duplicate entry found")
304 def finish(self
, axispos
):
305 if self
.axiscanvas
is not None: return
307 # temorarily enable the axis divisor
311 # lesspart and morepart can be called after defaultpart;
312 # this works although some axes may share their autoparting,
313 # because the axes are processed sequentially
315 if self
.parter
is not None:
316 min, max = self
.getrange()
317 self
.ticks
= tick
.mergeticklists(self
.manualticks
,
318 self
.parter
.defaultpart(min, max, not self
.fixmin
, not self
.fixmax
))
320 nextpart
= self
.parter
.lesspart
321 while nextpart
is not None:
322 newticks
= nextpart()
324 if newticks
is not None:
325 newticks
= tick
.mergeticklists(self
.manualticks
, newticks
)
328 bestrate
= self
.rater
.rateticks(self
, self
.ticks
, self
.density
)
329 bestrate
+= self
.rater
.raterange(self
.convert(self
.ticks
[-1])-
330 self
.convert(self
.ticks
[0]), 1)
331 variants
= [[bestrate
, self
.ticks
]]
337 newrate
= self
.rater
.rateticks(self
, newticks
, self
.density
)
338 newrate
+= self
.rater
.raterange(self
.convert(newticks
[-1])-
339 self
.convert(newticks
[0]), 1)
340 variants
.append([newrate
, newticks
])
341 if bestrate
is None or newrate
< bestrate
:
344 if worse
== self
.maxworse
and nextpart
== self
.parter
.lesspart
:
346 nextpart
= self
.parter
.morepart
347 if worse
== self
.maxworse
and nextpart
== self
.parter
.morepart
:
350 self
.ticks
=self
.manualticks
352 # rating, when several choises are available
355 if self
.painter
is not None:
358 while i
< len(variants
) and (bestrate
is None or variants
[i
][0] < bestrate
):
359 saverange
= self
._getrange
()
360 self
.ticks
= variants
[i
][1]
362 self
.setrange(self
.ticks
[0], self
.ticks
[-1])
363 self
.texter
.labels(self
.ticks
)
364 ac
= self
.painter
.paint(axispos
, self
)
365 ratelayout
= self
.rater
.ratelayout(ac
, self
.density
)
366 if ratelayout
is not None:
367 variants
[i
][0] += ratelayout
368 variants
[i
].append(ac
)
370 variants
[i
][0] = None
371 if variants
[i
][0] is not None and (bestrate
is None or variants
[i
][0] < bestrate
):
372 bestrate
= variants
[i
][0]
373 self
._forcerange
(saverange
)
376 raise RuntimeError("no valid axis partitioning found")
377 variants
= [variant
for variant
in variants
[:i
] if variant
[0] is not None]
379 self
.ticks
= variants
[0][1]
381 self
.setrange(self
.ticks
[0], self
.ticks
[-1])
382 self
.axiscanvas
= variants
[0][2]
384 self
.ticks
= variants
[0][1]
385 self
.texter
.labels(self
.ticks
)
387 self
.setrange(self
.ticks
[0], self
.ticks
[-1])
388 self
.axiscanvas
= painter
.axiscanvas()
391 self
.setrange(self
.ticks
[0], self
.ticks
[-1])
392 self
.texter
.labels(self
.ticks
)
393 if self
.painter
is not None:
394 self
.axiscanvas
= self
.painter
.paint(axispos
, self
)
396 self
.axiscanvas
= painter
.axiscanvas()
398 # disable the axis divisor
402 def createlinkaxis(self
, **args
):
403 return linked(self
, **args
)
406 class linear(_axis
, _linmap
):
407 """implementation of a linear axis"""
409 __implements__
= _Iaxis
411 def __init__(self
, parter
=parter
.autolinear(), rater
=rater
.linear(), **args
):
412 """initializes the instance
413 - the parter attribute implements _Iparter
414 - manualticks and the partitioner results are mixed
416 - the rater implements _Irater and is used to rate different
417 tick lists created by the partitioner (after merging with
419 - futher keyword arguments are passed to _axis"""
420 _axis
.__init
__(self
, **args
)
421 if self
.fixmin
and self
.fixmax
:
422 self
.relsize
= self
.max - self
.min
429 class logarithmic(_axis
, _logmap
):
430 """implementation of a logarithmic axis"""
432 __implements__
= _Iaxis
434 def __init__(self
, parter
=parter
.autologarithmic(), rater
=rater
.logarithmic(), **args
):
435 """initializes the instance
436 - the parter attribute implements _Iparter
437 - manualticks and the partitioner results are mixed
439 - the rater implements _Irater and is used to rate different
440 tick lists created by the partitioner (after merging with
442 - futher keyword arguments are passed to _axis"""
443 _axis
.__init
__(self
, **args
)
444 if self
.fixmin
and self
.fixmax
:
445 self
.relsize
= math
.log(self
.max) - math
.log(self
.min)
453 """base for a axis linked to an already existing axis
454 - almost all properties of the axis are "copied" from the
455 axis this axis is linked to
456 - usually, linked axis are used to create an axis to an
457 existing axis with different painting properties; linked
458 axis can be used to plot an axis twice at the opposite
459 sides of a graphxy or even to share an axis between
462 __implements__
= _Iaxis
464 def __init__(self
, linkedaxis
, painter
=painter
.linked()):
465 """initializes the instance
466 - it gets a axis this axis is linked to
467 - it gets a painter to be used for this linked axis"""
468 self
.linkedaxis
= linkedaxis
469 self
.painter
= painter
470 self
.axiscanvas
= None
472 def __getattr__(self
, attr
):
473 """access to unkown attributes are handed over to the
474 axis this axis is linked to"""
475 return getattr(self
.linkedaxis
, attr
)
477 def finish(self
, axispos
):
479 - instead of performing the hole finish process
480 (paritioning, rating, etc.) just a painter call
483 if self
.axiscanvas
is None:
484 if self
.linkedaxis
.axiscanvas
is None:
485 raise RuntimeError("link axis finish method called before the finish method of the original axis")
486 self
.axiscanvas
= self
.painter
.paint(axispos
, self
)
489 class linked(_linked
):
490 """a axis linked to an already existing regular axis
491 - adds divisor handling to _linked"""
493 __implements__
= _Iaxis
495 def finish(self
, axispos
):
496 # temporarily enable the linkedaxis divisor
497 self
.linkedaxis
.usedivisor
= 1
498 self
.linkedaxis
._setrange
()
500 _linked
.finish(self
, axispos
)
502 # disable the linkedaxis divisor again
503 self
.linkedaxis
.usedivisor
= 0
504 self
.linkedaxis
._setrange
()
508 """implementation of a split axis
509 - a split axis contains several (sub-)axes with
510 non-overlapping data ranges -- between these subaxes
511 the axis is "splitted"
512 - (just to get sure: a split axis can contain other
513 split axes as its subaxes)"""
515 __implements__
= _Iaxis
, painter
._Iaxispos
517 def __init__(self
, subaxes
, splitlist
=[0.5], splitdist
=0.1, relsizesplitdist
=1,
518 title
=None, painter
=painter
.split()):
519 """initializes the instance
520 - subaxes is a list of subaxes
521 - splitlist is a list of graph coordinates, where the splitting
522 of the main axis should be performed; if the list isn't long enough
523 for the subaxes, missing entries are considered to be None
524 - splitdist is the size of the splitting in graph coordinates, when
525 the associated splitlist entry is not None
526 - relsizesplitdist: a None entry in splitlist means, that the
527 position of the splitting should be calculated out of the
528 relsize values of conrtibuting subaxes (the size of the
529 splitting is relsizesplitdist in values of the relsize values
531 - title is the title of the axis as a string
532 - painter is the painter of the axis; it should be specialized to
534 - the relsize of the split axis is the sum of the relsizes of the
535 subaxes including the relsizesplitdist"""
536 self
.subaxes
= subaxes
537 self
.painter
= painter
539 self
.splitlist
= splitlist
540 for subaxis
in self
.subaxes
:
543 self
.subaxes
[0].vmin
= 0
544 self
.subaxes
[0].vminover
= None
545 self
.subaxes
[-1].vmax
= 1
546 self
.subaxes
[-1].vmaxover
= None
547 for i
in xrange(len(self
.splitlist
)):
548 if self
.splitlist
[i
] is not None:
549 self
.subaxes
[i
].vmax
= self
.splitlist
[i
] - 0.5*splitdist
550 self
.subaxes
[i
].vmaxover
= self
.splitlist
[i
]
551 self
.subaxes
[i
+1].vmin
= self
.splitlist
[i
] + 0.5*splitdist
552 self
.subaxes
[i
+1].vminover
= self
.splitlist
[i
]
554 while i
< len(self
.subaxes
):
555 if self
.subaxes
[i
].vmax
is None:
556 j
= relsize
= relsize2
= 0
557 while self
.subaxes
[i
+ j
].vmax
is None:
558 relsize
+= self
.subaxes
[i
+ j
].relsize
+ relsizesplitdist
560 relsize
+= self
.subaxes
[i
+ j
].relsize
561 vleft
= self
.subaxes
[i
].vmin
562 vright
= self
.subaxes
[i
+ j
].vmax
563 for k
in range(i
, i
+ j
):
564 relsize2
+= self
.subaxes
[k
].relsize
565 self
.subaxes
[k
].vmax
= vleft
+ (vright
- vleft
) * relsize2
/ float(relsize
)
566 relsize2
+= 0.5 * relsizesplitdist
567 self
.subaxes
[k
].vmaxover
= self
.subaxes
[k
+ 1].vminover
= vleft
+ (vright
- vleft
) * relsize2
/ float(relsize
)
568 relsize2
+= 0.5 * relsizesplitdist
569 self
.subaxes
[k
+1].vmin
= vleft
+ (vright
- vleft
) * relsize2
/ float(relsize
)
570 if i
== 0 and i
+ j
+ 1 == len(self
.subaxes
):
571 self
.relsize
= relsize
576 self
.fixmin
= self
.subaxes
[0].fixmin
578 self
.min = self
.subaxes
[0].min
579 self
.fixmax
= self
.subaxes
[-1].fixmax
581 self
.max = self
.subaxes
[-1].max
583 self
.axiscanvas
= None
586 min = self
.subaxes
[0].getrange()
587 max = self
.subaxes
[-1].getrange()
589 return min[0], max[1]
593 def setrange(self
, min, max):
594 self
.subaxes
[0].setrange(min, None)
595 self
.subaxes
[-1].setrange(None, max)
597 def adjustrange(self
, *args
, **kwargs
):
598 self
.subaxes
[0].adjustrange(*args
, **kwargs
)
599 self
.subaxes
[-1].adjustrange(*args
, **kwargs
)
601 def convert(self
, value
):
602 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
603 if value
< self
.subaxes
[0].max:
604 return self
.subaxes
[0].vmin
+ self
.subaxes
[0].convert(value
)*(self
.subaxes
[0].vmax
-self
.subaxes
[0].vmin
)
605 for axis
in self
.subaxes
[1:-1]:
606 if value
> axis
.min and value
< axis
.max:
607 return axis
.vmin
+ axis
.convert(value
)*(axis
.vmax
-axis
.vmin
)
608 if value
> self
.subaxes
[-1].min:
609 return self
.subaxes
[-1].vmin
+ self
.subaxes
[-1].convert(value
)*(self
.subaxes
[-1].vmax
-self
.subaxes
[-1].vmin
)
610 raise ValueError("value couldn't be assigned to a split region")
612 def finish(self
, axispos
):
613 if self
.axiscanvas
is None:
614 self
.axiscanvas
= self
.painter
.paint(axispos
, self
)
616 def createlinkaxis(self
, **args
):
617 return linkedsplit(self
, **args
)
620 class omitsubaxispainter
: pass
622 class linkedsplit(_linked
):
623 """a split axis linked to an already existing split axis
624 - inherits the access to a linked axis -- as before,
625 basically only the painter is replaced
626 - it takes care of the creation of linked axes of
629 __implements__
= _Iaxis
631 def __init__(self
, linkedaxis
, painter
=painter
.linkedsplit(), subaxispainter
=omitsubaxispainter
):
632 """initializes the instance
633 - linkedaxis is the axis this axis becomes linked to
634 - painter is axispainter instance for this linked axis
635 - subaxispainter is a changeable painter to be used for linked
636 subaxes; if omitsubaxispainter the createlinkaxis method of
637 the subaxis are called without a painter parameter"""
638 _linked
.__init
__(self
, linkedaxis
, painter
=painter
)
640 for subaxis
in linkedaxis
.subaxes
:
641 painter
= attr
.selectattr(subaxispainter
, len(self
.subaxes
), len(linkedaxis
.subaxes
))
642 if painter
is omitsubaxispainter
:
643 self
.subaxes
.append(subaxis
.createlinkaxis())
645 self
.subaxes
.append(subaxis
.createlinkaxis(painter
=painter
))
649 """implementation of a axis for bar graphs
650 - a bar axes is different from a split axis by the way it
651 selects its subaxes: the convert method gets a list,
652 where the first entry is a name selecting a subaxis out
653 of a list; instead of the term "bar" or "subaxis" the term
654 "item" will be used here
655 - the bar axis stores a list of names be identify the items;
656 the names might be of any time (strings, integers, etc.);
657 the names can be printed as the titles for the items, but
658 alternatively the names might be transformed by the texts
659 dictionary, which maps a name to a text to be used to label
660 the items in the painter
661 - usually, there is only one subaxis, which is used as
662 the subaxis for all items
663 - alternatively it is also possible to use another bar axis
664 as a multisubaxis; it is copied via the createsubaxis
665 method whenever another subaxis is needed (by that a
666 nested bar axis with a different number of subbars at
667 each item can be created)
668 - any axis can be a subaxis of a bar axis; if no subaxis
669 is specified at all, the bar axis simulates a linear
670 subaxis with a fixed range of 0 to 1"""
672 def __init__(self
, subaxis
=None, multisubaxis
=None,
673 dist
=0.5, firstdist
=None, lastdist
=None,
674 names
=None, texts
={},
675 title
=None, painter
=painter
.bar()):
676 """initialize the instance
677 - subaxis contains a axis to be used as the subaxis
679 - multisubaxis might contain another bar axis instance
680 to be used to construct a new subaxis for each item;
681 (by that a nested bar axis with a different number
682 of subbars at each item can be created)
683 - only one of subaxis or multisubaxis can be set; if neither
684 of them is set, the bar axis behaves like having a linear axis
685 as its subaxis with a fixed range 0 to 1
686 - the title attribute contains the axis title as a string
687 - the dist is a relsize to be used as the distance between
689 - the firstdist and lastdist are the distance before the
690 first and after the last item, respectively; when set
691 to None (the default), 0.5*dist is used
692 - the relsize of the bar axis is the sum of the
693 relsizes including all distances between the items"""
695 if firstdist
is not None:
696 self
.firstdist
= firstdist
698 self
.firstdist
= 0.5 * dist
699 if lastdist
is not None:
700 self
.lastdist
= lastdist
702 self
.lastdist
= 0.5 * dist
704 self
.multisubaxis
= multisubaxis
705 if self
.multisubaxis
is not None:
706 if subaxis
is not None:
707 raise RuntimeError("either use subaxis or multisubaxis")
710 self
.subaxis
= subaxis
712 self
.painter
= painter
713 self
.axiscanvas
= None
716 def createsubaxis(self
):
717 return bar(subaxis
=self
.multisubaxis
.subaxis
,
718 multisubaxis
=self
.multisubaxis
.multisubaxis
,
719 title
=self
.multisubaxis
.title
,
720 dist
=self
.multisubaxis
.dist
,
721 firstdist
=self
.multisubaxis
.firstdist
,
722 lastdist
=self
.multisubaxis
.lastdist
,
723 painter
=self
.multisubaxis
.painter
)
726 # TODO: we do not yet have a proper range handling for a bar axis
729 def setrange(self
, min=None, max=None):
730 # TODO: we do not yet have a proper range handling for a bar axis
731 raise RuntimeError("range handling for a bar axis is not implemented")
733 def setname(self
, name
, *subnames
):
734 """add a name to identify an item at the bar axis
735 - by using subnames, nested name definitions are
737 - a style (or the user itself) might use this to
738 insert new items into a bar axis
739 - setting self.relsizes to None forces later recalculation"""
740 if name
not in self
.names
:
742 self
.names
.append(name
)
743 if self
.multisubaxis
is not None:
744 self
.subaxis
.append(self
.createsubaxis())
746 if self
.multisubaxis
is not None:
747 if self
.subaxis
[self
.names
.index(name
)].setname(*subnames
):
750 if self
.subaxis
.setname(*subnames
):
752 return self
.relsizes
is not None
754 def adjustrange(self
, points
, index
, subnames
=[]):
756 if index
is not None:
757 self
.setname(point
[index
], *subnames
)
759 self
.setname(point
, *subnames
)
761 def updaterelsizes(self
):
762 # guess what it does: it recalculates relsize attribute
763 self
.relsizes
= [i
*self
.dist
+ self
.firstdist
for i
in range(len(self
.names
) + 1)]
764 self
.relsizes
[-1] += self
.lastdist
- self
.dist
765 if self
.multisubaxis
is not None:
767 for i
in range(1, len(self
.relsizes
)):
768 self
.subaxis
[i
-1].updaterelsizes()
769 subrelsize
+= self
.subaxis
[i
-1].relsizes
[-1]
770 self
.relsizes
[i
] += subrelsize
772 if self
.subaxis
is None:
775 self
.subaxis
.updaterelsizes()
776 subrelsize
= self
.subaxis
.relsizes
[-1]
777 for i
in range(1, len(self
.relsizes
)):
778 self
.relsizes
[i
] += i
* subrelsize
780 def convert(self
, value
):
781 """bar axis convert method
782 - the value should be a list, where the first entry is
783 a member of the names (set in the constructor or by the
784 setname method); this first entry identifies an item in
786 - following values are passed to the appropriate subaxis
788 - when there is no subaxis, the convert method will behave
789 like having a linear axis from 0 to 1 as subaxis"""
790 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
791 if not self
.relsizes
:
792 self
.updaterelsizes()
793 pos
= self
.names
.index(value
[0])
795 if self
.subaxis
is None:
798 if self
.multisubaxis
is not None:
799 subvalue
= value
[1] * self
.subaxis
[pos
].relsizes
[-1]
801 subvalue
= value
[1] * self
.subaxis
.relsizes
[-1]
803 if self
.multisubaxis
is not None:
804 subvalue
= self
.subaxis
[pos
].convert(value
[1:]) * self
.subaxis
[pos
].relsizes
[-1]
806 subvalue
= self
.subaxis
.convert(value
[1:]) * self
.subaxis
.relsizes
[-1]
807 return (self
.relsizes
[pos
] + subvalue
) / float(self
.relsizes
[-1])
809 def finish(self
, axispos
):
810 if self
.axiscanvas
is None:
811 if self
.multisubaxis
is not None:
812 for name
, subaxis
in zip(self
.names
, self
.subaxis
):
813 subaxis
.vmin
= self
.convert((name
, 0))
814 subaxis
.vmax
= self
.convert((name
, 1))
815 self
.axiscanvas
= self
.painter
.paint(axispos
, self
)
817 def createlinkaxis(self
, **args
):
818 return linkedbar(self
, **args
)
821 class linkedbar(_linked
):
822 """a bar axis linked to an already existing bar axis
823 - inherits the access to a linked axis -- as before,
824 basically only the painter is replaced
825 - it must take care of the creation of linked axes of
828 __implements__
= _Iaxis
830 def __init__(self
, linkedaxis
, painter
=painter
.linkedbar()):
831 """initializes the instance
832 - it gets a axis this linkaxis is linked to
833 - it gets a painter to be used for this linked axis"""
834 _linked
.__init
__(self
, linkedaxis
, painter
=painter
)
835 if self
.multisubaxis
is not None:
836 self
.subaxis
= [subaxis
.createlinkaxis() for subaxis
in self
.linkedaxis
.subaxis
]
837 elif self
.subaxis
is not None:
838 self
.subaxis
= self
.subaxis
.createlinkaxis()
841 def pathaxis(path
, axis
, **kwargs
):
842 """creates an axiscanvas for an axis along a path"""
843 axis
.finish(painter
.pathaxispos(path
, axis
.convert
, **kwargs
))
844 return axis
.axiscanvas